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
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.
Also Liked
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
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
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
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









