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

Where Next?

Popular in Questions Top

New
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
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
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
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
dotdotdotPaul
Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31013 112
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
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
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
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New

We're in Beta

About us Mission Statement