Skip to content

Instantly share code, notes, and snippets.

@khanha2
Last active January 20, 2024 06:41
Show Gist options
  • Save khanha2/eae44e5b534c141ce00b30416851cc18 to your computer and use it in GitHub Desktop.
Save khanha2/eae44e5b534c141ce00b30416851cc18 to your computer and use it in GitHub Desktop.
How Enum.at work

Analyze the Enum.at function

Introduction

Enum.at/3 function is used to find an element at the given index in zero-based. This document describe how this function work and explain it complexity.

Analysis

The Enum.at/3 is defined in the official elixir repository at this code: https://github.com/elixir-lang/elixir/blob/d3285b176e87b025a6949867b58a7ce0e3839b58/lib/elixir/lib/enum.ex#L478

def at(enumerable, index, default \\ nil) when is_integer(index) do
  case slice_forward(enumerable, index, 1, 1) do
    [value] -> value
    [] -> default
  end
end

The slice_forward/4 function is defined at this code: https://github.com/elixir-lang/elixir/blob/d3285b176e87b025a6949867b58a7ce0e3839b58/lib/elixir/lib/enum.ex#L4460

defp slice_forward(enumerable, start, amount, step) when start < 0 do
  {count, fun} = slice_count_and_fun(enumerable, step)
  start = count + start

  if start >= 0 do
    amount = Kernel.min(amount, count - start)
    amount = amount_with_step(amount, step)
    fun.(start, amount, step)
  else
    []
  end
end

defp slice_forward(list, start, amount, step) when is_list(list) do
  amount = amount_with_step(amount, step)
  slice_list(list, start, amount, step)
end

defp slice_forward(enumerable, start, amount, step) do
  case Enumerable.slice(enumerable) do
    {:ok, count, _} when start >= count ->
      []

    {:ok, count, fun} when is_function(fun, 1) ->
      amount = Kernel.min(amount, count - start) |> amount_with_step(step)
      enumerable |> fun.() |> slice_exact(start, amount, step, count)

    # TODO: Deprecate me in Elixir v1.18.
    {:ok, count, fun} when is_function(fun, 2) ->
      amount = Kernel.min(amount, count - start)

      if step == 1 do
        fun.(start, amount)
      else
        fun.(start, Kernel.min(amount * step, count - start))
        |> take_every_list(amount, step - 1)
      end

    {:ok, count, fun} when is_function(fun, 3) ->
      amount = Kernel.min(amount, count - start) |> amount_with_step(step)
      fun.(start, amount, step)

    {:error, module} ->
      slice_enum(enumerable, module, start, amount, step)
  end
end

As we can see, the second pattern matches this condition: the enumerable is a list.

Take a look at the function that match the second condition:

defp slice_forward(list, start, amount, step) when is_list(list) do
  amount = amount_with_step(amount, step)
  slice_list(list, start, amount, step)
end

This function call 2 functions amount_with_step/2 and slice_list/4 to slide the list.

The amount_with_step/2 is defined at this code: https://github.com/elixir-lang/elixir/blob/d3285b176e87b025a6949867b58a7ce0e3839b58/lib/elixir/lib/enum.ex#L3042

defp amount_with_step(amount, 1), do: amount
defp amount_with_step(amount, step), do: div(amount - 1, step) + 1

The slice_list/4 is defined at this code: https://github.com/elixir-lang/elixir/blob/d3285b176e87b025a6949867b58a7ce0e3839b58/lib/elixir/lib/enum.ex#L4507

defp slice_list(list, start, amount, step) do
  if step == 1 do
    list |> drop_list(start) |> take_list(amount)
  else
    list |> drop_list(start) |> take_every_list(amount, step - 1)
  end
end

The drop_list/2 is defined at this code: https://github.com/elixir-lang/elixir/blob/d3285b176e87b025a6949867b58a7ce0e3839b58/lib/elixir/lib/enum.ex#L4272

defp drop_list(list, 0), do: list
defp drop_list([_ | tail], counter), do: drop_list(tail, counter - 1)
defp drop_list([], _), do: []

The take_list/2 and take_every_list/3 ar defined at this code: https://github.com/elixir-lang/elixir/blob/d3285b176e87b025a6949867b58a7ce0e3839b58/lib/elixir/lib/enum.ex#L4755

defp take_list(_list, 0), do: []
defp take_list([head | tail], counter), do: [head | take_list(tail, counter - 1)]
defp take_list([], _counter), do: []

defp take_every_list([head | tail], to_drop),
do: [head | tail |> drop_list(to_drop) |> take_every_list(to_drop)]

defp take_every_list([], _to_drop), do: []

defp take_every_list(_list, 0, _to_drop), do: []

defp take_every_list([head | tail], counter, to_drop),
do: [head | tail |> drop_list(to_drop) |> take_every_list(counter - 1, to_drop)]

defp take_every_list([], _counter, _to_drop), do: []

We simulate the Enum.at/3 function:

defmodule SimulateEnumAt do
  def perform(list, index) do
    amount = amount_with_step(1, 1)

    case slice_list(list, index, amount, 1) do
      [] -> nil
      [value] -> value
    end
  end

  defp amount_with_step(amount, 1) do
    IO.inspect("amount_with_step with the step is 1")
    amount
  end

  defp amount_with_step(amount, step) do
    IO.inspect("amount_with_step with the remaining condition")
    div(amount - 1, step) + 1
  end

  defp slice_list(list, start, amount, step) do
    if step == 1 do
      IO.inspect("slice_list with the step is 1")
      list |> drop_list(start) |> take_list(amount)
    else
      IO.inspect("slice_list with the remaining condition")
      list |> drop_list(start) |> take_every_list(amount, step - 1)
    end
  end

  defp drop_list(list, 0) do
    IO.inspect("drop_list with the counter is 0")
    list
  end

  defp drop_list([_ | tail] = list, counter) do
    IO.inspect("drop_list with the list is #{inspect(list)}, the counter is #{counter}")
    drop_list(tail, counter - 1)
  end

  defp drop_list([], _) do
    IO.inspect("drop_list with the list is empty")
    []
  end

  defp take_list(_list, 0) do
    IO.inspect("take_list with the counter is 0")
    []
  end

  defp take_list([head | tail] = list, counter) do
    IO.inspect("take_list with the list is #{inspect(list)}, the counter is #{counter}")
    [head | take_list(tail, counter - 1)]
  end

  defp take_list([], _counter) do
    IO.inspect("take_list with the list is empty")
    []
  end

  defp take_every_list(_list, 0, _to_drop) do
    IO.inspect("take_every_list with counter with the counter is 0")
    []
  end

  defp take_every_list([head | tail] = list, counter, to_drop) do
    IO.inspect("take_every_list with counter with the list is #{inspect(list)}, the counter is #{counter}, the to_drop is #{to_drop}}")
    [head | tail |> drop_list(to_drop) |> take_every_list(counter - 1, to_drop)]
  end

  defp take_every_list([], _counter, _to_drop) do
    IO.inspect("take_every_list with counter with the list is empty")
    []
  end
end

Run the simulated function

iex> SimulateEnumAt.perform(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"], 8)
"amount_with_step with the step is 1"
"slice_list with the step is 1"
"drop_list with the list is [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"], the counter is 8"
"drop_list with the list is [\"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"], the counter is 7"
"drop_list with the list is [\"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"], the counter is 6"
"drop_list with the list is [\"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"], the counter is 5"
"drop_list with the list is [\"e\", \"f\", \"g\", \"h\", \"i\", \"j\"], the counter is 4"
"drop_list with the list is [\"f\", \"g\", \"h\", \"i\", \"j\"], the counter is 3"
"drop_list with the list is [\"g\", \"h\", \"i\", \"j\"], the counter is 2"
"drop_list with the list is [\"h\", \"i\", \"j\"], the counter is 1"
"drop_list with the counter is 0"
"take_list with the list is [\"i\", \"j\"], the counter is 1"
"take_list with the counter is 0"
"i"

As we can see, the function loop the list to find the element that match the index by using the drop_list/2 function to drop the first element when the counter is decreased. That mean if we want to find an element by it index, we need index step to loop the list. The worst case is the index is greater than or equal to the number of element. The reason for this solution is the list structure in Elixir is built for real Linked list, so the only way to find an element in a linked list is looping from the first to the last element to find the result. The complexity for this solution is O(n), which n is the number of elements.

Reference

Elixir List document: https://hexdocs.pm/elixir/1.14/List.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment