Interview

10 Elixir Interview Questions and Answers

Prepare for your next technical interview with this guide on Elixir, covering core concepts and practical applications to enhance your understanding.

Elixir is a dynamic, functional language designed for building scalable and maintainable applications. Leveraging the Erlang VM, Elixir offers impressive performance and fault-tolerance, making it a popular choice for concurrent and distributed systems. Its syntax is clean and modern, which, combined with powerful metaprogramming capabilities, allows developers to write expressive and efficient code.

This article provides a curated selection of Elixir interview questions to help you prepare effectively. By working through these questions, you will gain a deeper understanding of Elixir’s core concepts and practical applications, positioning yourself as a strong candidate in technical interviews.

Elixir Interview Questions and Answers

1. Write a tail-recursive function to calculate the factorial of a number.

In Elixir, tail recursion is a form of recursion where the recursive call is the last operation in the function. This allows the compiler to optimize the recursion, preventing stack overflow errors and improving performance. Tail-recursive functions are particularly useful in functional programming languages like Elixir, where immutability and recursion are common patterns.

Here is an example of a tail-recursive function to calculate the factorial of a number in Elixir:

defmodule Factorial do
  def calculate(n), do: calculate(n, 1)

  defp calculate(0, acc), do: acc
  defp calculate(n, acc) when n > 0 do
    calculate(n - 1, n * acc)
  end
end

IO.puts Factorial.calculate(5)  # Output: 120

In this example, the calculate/2 function is the tail-recursive helper function. It takes two arguments: the number n and an accumulator acc that holds the intermediate result. The base case is when n is 0, at which point the accumulator contains the final result. The recursive case multiplies the current number n with the accumulator and decrements n by 1, making the recursive call the last operation.

2. Implement a basic GenServer that maintains a counter and provides increment and decrement functionalities.

A GenServer in Elixir is a generic server process that abstracts the common patterns of a server. It is used to maintain state, handle synchronous and asynchronous calls, and manage the lifecycle of a process. In this example, we will implement a basic GenServer that maintains a counter and provides increment and decrement functionalities.

defmodule Counter do
  use GenServer

  # Client API
  def start_link(initial_value \\ 0) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end

  def increment do
    GenServer.call(__MODULE__, :increment)
  end

  def decrement do
    GenServer.call(__MODULE__, :decrement)
  end

  def get_value do
    GenServer.call(__MODULE__, :get_value)
  end

  # Server Callbacks
  def init(initial_value) do
    {:ok, initial_value}
  end

  def handle_call(:increment, _from, state) do
    {:reply, state + 1, state + 1}
  end

  def handle_call(:decrement, _from, state) do
    {:reply, state - 1, state - 1}
  end

  def handle_call(:get_value, _from, state) do
    {:reply, state, state}
  end
end

# Usage
{:ok, _pid} = Counter.start_link(10)
Counter.increment()
Counter.decrement()
Counter.get_value()

3. Use streams to read a large file line-by-line and count the number of lines containing a specific word.

In Elixir, streams are used to handle large files efficiently by processing data lazily. This means that the file is read line-by-line, and each line is processed as it is read, rather than loading the entire file into memory.

Here is an example of how to use streams to read a large file line-by-line and count the number of lines containing a specific word:

defmodule LineCounter do
  def count_lines_with_word(file_path, word) do
    File.stream!(file_path)
    |> Stream.filter(&String.contains?(&1, word))
    |> Enum.count()
  end
end

# Usage
LineCounter.count_lines_with_word("large_file.txt", "specific_word")

In this example, File.stream!/1 is used to create a stream from the file. The Stream.filter/2 function is then used to filter lines that contain the specific word. Finally, Enum.count/1 is used to count the number of lines that match the condition.

4. Create an ETS table to store user sessions and demonstrate how to insert and retrieve a session.

ETS (Erlang Term Storage) is a powerful storage system for storing large amounts of data in-memory. It is commonly used in Elixir for tasks that require fast read and write access. ETS tables can be used to store user sessions efficiently.

Example:

# Create an ETS table
:ets.new(:sessions, [:named_table, :public, :set])

# Insert a session
:ets.insert(:sessions, {:user1, %{session_id: "abc123", data: "some_data"}})

# Retrieve a session
case :ets.lookup(:sessions, :user1) do
  [{:user1, session}] -> IO.inspect(session)
  [] -> IO.puts("Session not found")
end

5. Describe the steps involved in performing a hot code upgrade in an Elixir application.

Performing a hot code upgrade in an Elixir application involves several steps to ensure that the application can be updated without downtime. Here is a high-level overview of the process:

  • Prepare the Release: First, create a release of your application using tools like Distillery or Mix. The release includes all the necessary files and configurations required to run your application.
  • Versioning: Ensure that your application version is incremented. The upgrade process relies on version numbers to determine what changes need to be applied.
  • Generate Upgrade Instructions: Use the release tool to generate upgrade instructions. These instructions will include the changes between the current version and the new version of your application.
  • Deploy the Upgrade: Transfer the new release and the upgrade instructions to the running system. This can be done using various deployment strategies, such as copying files to the server or using a deployment tool.
  • Apply the Upgrade: Finally, apply the upgrade instructions to the running system. This step involves telling the running Erlang VM to load the new code and apply any necessary changes. This is typically done using commands provided by the release tool.

6. Write an ExUnit test case to verify the functionality of a module that calculates the sum of a list of numbers.

To verify the functionality of a module that calculates the sum of a list of numbers in Elixir, you can use ExUnit, Elixir’s built-in test framework. Below is an example of how to write an ExUnit test case for this purpose.

First, let’s assume we have a module named SumList with a function calculate/1 that takes a list of numbers and returns their sum.

defmodule SumList do
  def calculate(numbers) do
    Enum.sum(numbers)
  end
end

Now, we can write an ExUnit test case to verify the functionality of this module.

defmodule SumListTest do
  use ExUnit.Case

  test "calculates the sum of a list of numbers" do
    assert SumList.calculate([1, 2, 3, 4, 5]) == 15
    assert SumList.calculate([10, 20, 30]) == 60
    assert SumList.calculate([]) == 0
  end
end

7. Create a schema and changeset using Ecto for a User model with fields: name, email, and age.

In Elixir, Ecto is a domain-specific language for writing queries and interacting with databases. A schema in Ecto is used to map data from a database table to an Elixir struct. Changesets are used to cast and validate data before it is inserted or updated in the database.

Here is an example of how to create a schema and changeset for a User model with fields: name, email, and age.

defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :age])
    |> validate_required([:name, :email, :age])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0)
  end
end

8. Describe how to use Task.async and Task.await for concurrent processing.

In Elixir, Task.async and Task.await are used to perform concurrent processing. Task.async is used to start a task asynchronously, while Task.await is used to wait for the result of the asynchronous task.

Example:

defmodule ConcurrentExample do
  def async_task do
    task = Task.async(fn -> perform_heavy_computation() end)
    result = Task.await(task)
    IO.puts("Result: #{result}")
  end

  defp perform_heavy_computation do
    # Simulate a heavy computation
    :timer.sleep(2000)
    42
  end
end

ConcurrentExample.async_task()

In this example, the perform_heavy_computation function is executed asynchronously using Task.async. The main process then waits for the result using Task.await, allowing other operations to be performed concurrently while waiting for the computation to complete.

9. Write a custom Mix task that prints “Hello, World!” to the console.

Mix is a build tool that provides tasks for creating, compiling, and testing Elixir projects. Custom Mix tasks can be created to automate various development and build processes. Here is an example of how to create a custom Mix task that prints “Hello, World!” to the console.

# lib/mix/tasks/hello.ex
defmodule Mix.Tasks.Hello do
  use Mix.Task

  @shortdoc "Prints 'Hello, World!' to the console"
  def run(_) do
    IO.puts("Hello, World!")
  end
end

To run this custom Mix task, you would use the following command in your terminal:

mix hello

10. Demonstrate how to use Mox to mock a dependency in an ExUnit test.

Mox is a library in Elixir that allows you to create mocks for testing purposes. It is particularly useful when you want to isolate the functionality of a module by mocking its dependencies. This helps in writing unit tests that are not dependent on external systems or complex setups.

To use Mox, you need to follow these steps:

  • Add Mox as a dependency in your mix.exs file.
  • Define a behavior that your module will implement.
  • Create a mock for the behavior using Mox.
  • Use the mock in your ExUnit test.

Example:

# mix.exs
defp deps do
  [
    {:mox, "~> 1.0", only: :test}
  ]
end

# lib/my_app/my_behavior.ex
defmodule MyApp.MyBehavior do
  @callback my_function(arg :: any()) :: any()
end

# lib/my_app/my_module.ex
defmodule MyApp.MyModule do
  @behaviour MyApp.MyBehavior

  def my_function(arg) do
    # Implementation
  end
end

# test/test_helper.exs
ExUnit.start()
Mox.defmock(MyApp.MyBehaviorMock, for: MyApp.MyBehavior)

# test/my_app/my_module_test.exs
defmodule MyApp.MyModuleTest do
  use ExUnit.Case, async: true
  import Mox

  setup :verify_on_exit!

  test "my_function/1 uses the mock" do
    MyApp.MyBehaviorMock
    |> expect(:my_function, fn _arg -> :mocked_response end)

    assert MyApp.MyModule.my_function(:test_arg) == :mocked_response
  end
end
Previous

10 Clean Architecture Interview Questions and Answers

Back to Interview
Next

10 Offensive Security Interview Questions and Answers