Dusty

Dusty

How expensive is list reversal in Elixir?

One of the Exercism exercises on the Elixir track is to take an integer (0 to ~3000) and convert it to a Roman numeral. My solution was a little “pipe-happy,” and I have subsequently discovered better approaches. In the course of doing all that piping, I used Enum.reverse() twice. I’ve read that sometimes list reversal is O(n) and sometimes O(1) with a pointer swap, but I don’t know which applies in Elixir. Should I even care? Or perhaps a better question, at what length of list should I start caring?

For reference, my solution:

defmodule RomanNumerals do
  @doc """
  Convert the number to a roman number.
  """
  @spec numeral(pos_integer) :: String.t()
  def numeral(number) when is_integer(number) do
    number
    |> Integer.digits()
    |> Enum.reverse()
    |> Enum.with_index()
    |> Enum.map(&get_roman/1)
    |> Enum.reverse()
    |> List.to_string()
  end

  def get_roman({_number, _index} = pair) do
    case pair do
      {0, _} -> ""
      {1, 0} -> "I"
      {2, 0} -> "II"
      {3, 0} -> "III"
      {4, 0} -> "IV"
      {5, 0} -> "V"
      {6, 0} -> "VI"
      {7, 0} -> "VII"
      {8, 0} -> "VIII"
      {9, 0} -> "IX"
      {1, 1} -> "X"
      {2, 1} -> "XX"
      {3, 1} -> "XXX"
      {4, 1} -> "XL"
      {5, 1} -> "L"
      {6, 1} -> "LX"
      {7, 1} -> "LXX"
      {8, 1} -> "LXXX"
      {9, 1} -> "XC"
      {1, 2} -> "C"
      {2, 2} -> "CC"
      {3, 2} -> "CCC"
      {4, 2} -> "CD"
      {5, 2} -> "D"
      {6, 2} -> "DC"
      {7, 2} -> "DCC"
      {8, 2} -> "DCCC"
      {9, 2} -> "CM"
      {1, 3} -> "M"
      {2, 3} -> "MM"
      {3, 3} -> "MMM"
      _ -> "Too High!"
    end
  end
end

And a more elegant solution by one of the other students:

defmodule RomanNumerals do
  @doc """
  Convert the number to a roman number.
  """
  @spec numeral(pos_integer) :: String.t()
  def numeral(n) do
    cond do
      n >= 1000 -> "M"  <> numeral(n - 1000)
      n >= 900  -> "CM" <> numeral(n - 900)
      n >= 500  -> "D"  <> numeral(n - 500)
      n >= 400  -> "CD" <> numeral(n - 400)
      n >= 100  -> "C"  <> numeral(n - 100)
      n >= 90   -> "XC" <> numeral(n - 90)
      n >= 50   -> "L"  <> numeral(n - 50)
      n >= 40   -> "XL" <> numeral(n - 40)
      n >= 10   -> "X"  <> numeral(n - 10)
      n >= 9    -> "IX" <> numeral(n - 9)
      n >= 5    -> "V"  <> numeral(n - 5)
      n >= 4    -> "IV" <> numeral(n - 4)
      n >= 1    -> "I"  <> numeral(n - 1)
      true      -> ""
    end
  end
end

Marked As Solved

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

List reversal is indeed O(n) , however it is also a built in optimized function within the VM. List reversal happens constantly within functional languages, so the VM goes to a lot of effort optimize it.

13
Post #3

Also Liked

hauleth

hauleth

Yes, in this case it can be only from the outer scope as “function” n do not exists at all. It would probably be more clear if I would write it as:

  for {num, roman} <- @digits do
    defp do_numeral(n, list) when n >= unquote(num),
      do: do_numeral(n - unquote(num), [list | unquote(roman)])
  end
hauleth

hauleth

About Your question about “better solution” then when building binaries the answer almost always will be the same - io lists.

defmodule RomanNumerals do
  @digits [
    {1000, "M"},
    {900, "CM"},
    {500, "D"},
    {400, "CD"},
    {100, "C"},
    {90, "XC"},
    {50, "L"},
    {40, "XL"},
    {10, "X"},
    {9, "IX"},
    {5, "V"},
    {4, "IV"},
    {1, "I"}
  ]
  @doc """
  Convert the number to a roman number.
  """
  @spec numeral(pos_integer) :: String.t()
  def numeral(n), do: do_numeral(n, [])

  defp do_numeral(0, list), do: List.to_string(list)

  for {n, d} <- @digits do
    defp do_numeral(n, list) when n >= unquote(n),
      do: do_numeral(n - unquote(n), [list | unquote(d)])
  end
end
hauleth

hauleth

There is “implicit macro” which is in def/defp. You can think about unquote/1 in this case as a way to use variable from “outer scope”:

defmodule Foo do
  foo = 1

  def foo, do: unquote(foo)
end

Foo.foo #=> 1

So in this sense it is a way to use bindings defined in for comprehension within function definition. It is used to have dynamically defined functions via meta programming. And sometimes it is required to be able to use custom atom as function name:

defmodule Foo do
  def unquote(:"foo-bar"), do: 42
end

Which is sometimes needed for modules that are meant to work for example with xmerl or '$handle_undefined_function'/2 handler, ex:

defmodule Foo do
  def unquote(:"$handle_undefined_function")(f, a), do: IO.inspect({f, a})
end

Foo.bar
StanBright

StanBright

To be honest, I don’t know whether it is O(n) or O(1). I have a feeling that it is O(n). I’d be happy if someone more experienced chimes in.

At the same time, I did an exercise on Exercism recently that required a “fast” solution. It required working with a list of 1_000_000 items and reversing the list was fast enough. Reversing through tail recursion. I think that Enum.reverse uses a similar approach if not something faster :).

Dusty

Dusty

This is a fascinating solution :slight_smile:

Can you explain the use of unquote() in this context? I don’t think I’ve seen it outside of a defmacro quote.

Where Next?

Popular in Questions Top

albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement