orestis

orestis

Newcomer to Elixir, is this idiomatic code? (advent of code solution)

So I’ve started learning Elixir. I took on the Advent of Code challenges, and solved the first day. I’ve skimmed a few books on elixir/erlang, and have had some exposure to FP constructs (including a pleasant semester of Prolog at uni 10 years ago :slight_smile: ).

I came up with the following solution to the day 1 problem. The two test functions showcase what the requirements are (and hopefully the implementation is written such a way what it will be immediately obvious).

Now, after I solved this I searched online to see what other people did. Here are some linked examples:

Reddit Thread
Github 1
Github 2

In those links I see the obvious Enum.reduce approach plus some approaches that seem really weird. I took a classic recursive approach with pattern matching, but only because it’s the feature I prominently remember and I didn’t remember how to write the case construct, so I wanted to see if it would be possible. I have to confess when this code worked, I giggled a bit :slight_smile:

So my question is, is this code considered idiomatic Elixir? The Pythonista in me really likes this code, even if it is a bit more verbose than other solutions. Could I rewrite it to be more idiomatic but keeping the same recursive approach?

Thanks!

defmodule Day1 do
  def santa(s) do
    l = String.codepoints(s)
    santa(l, 0)
  end

  defp santa([], n) do
    n
  end

  defp santa(["(" | tail], n) do
    santa(tail, n + 1)
  end

  defp santa([")" | tail], n) do
    santa(tail, n - 1)
  end

  def test_santa do 
    IO.puts(santa("(())") == 0)
    IO.puts(santa("()()") == 0)
    IO.puts(santa("(((") == 3)
    IO.puts(santa("(()(()(") == 3)
    IO.puts(santa("))(((((") == 3)
    IO.puts(santa("())") == -1)
    IO.puts(santa("))(") == -1)
    IO.puts(santa(")))") == -3)
    IO.puts(santa(")())())") == -3)
  end

  def solve do 
    IO.puts "and now, the punchline"
    {:ok, input} = File.read "day1.input.txt"
    IO.puts(santa input)
    IO.puts(basement input)
  end

  def basement(s) do
    basement(String.codepoints(s), 0, 0)
  end

  defp basement(_, pos, -1) do
    pos
  end

  defp basement(["(" | tail], pos, floor) do
    basement(tail, pos + 1, floor + 1)
  end

  defp basement([")" | tail], pos, floor) do
    basement(tail, pos + 1, floor - 1)
  end

  def test_basement do
    IO.puts(basement(")") == 1)
    IO.puts(basement("()())") == 5)
  end

end


Day1.solve

Most Liked

OvermindDL1

OvermindDL1

I prefer the classic recursive approach as well, though I’d probably align the santa/2 parts to be more like:

  defp santa([], n)           ,do: n
  defp santa(["(" | tail], n) ,do: santa(tail, n + 1)
  defp santa([")" | tail], n) ,do: santa(tail, n - 1)

Two things though, I notice no clause to handle a non-parenthesis, and this is fine for small inputs but huge inputs then might be better without TCO, basically this:

  def santa(s) ,do: s
    |> String.codepoints
    |> do_santa

  defp do_santa([])           ,do: 0
  defp do_santa(["(" | tail]) ,do: santa(tail) + 1
  defp do_santa([")" | tail]) ,do: santa(tail) - 1

All the above code is untested, but is just my way of doing it. :slight_smile:

Also, instead of the testing you are doing there this would be an almost trivial example case for a property testing library if you want to learn something fascinating and useful. :slight_smile:

gregvaughn

gregvaughn

I think that manual recursion approach is something everyone needs to know how to do, and it’s great as a learning exercise. That said, however, I’d expect something more like this in production code:

 def santa(s) do
    s |> String.codepoints
      |> Enum.reduce(0, 
        fn "(", n -> n + 1
           ")", n -> n - 1
        end)
  end
OvermindDL1

OvermindDL1

Correct, but TCO is not free, and I was typing up a long post but I found a good conversation about it on these forums so deleted it and here is the post that describes it in detail and ‘why’ it is the way it is: Tail Call Optimization in Elixir & Erlang – not as efficient and important as you probably think

In essence, an EVM (Erlang VM) process’s stack is also on the heap and they grow toward each other, so it does not matter which you use from a memory perspective, and overall it is more important to keep memory changes down. So in this specific case TCO would not really hurt, but it is not necessary and would likely be faster without TCO since you have bounded inputs.

The only time TCO should really be focused on is when, as per @rvirding as I recall, only when you have unbounded loops, like a receive loop.

Last Post!

gregvaughn

gregvaughn

Agreed. Technically you do pattern match even in case statements, but yes, there’s a general preference for doing it in function heads (even anonymous function heads). I have run across a circumstance where I felt the code read better by using a case rather than splitting things up across multiple function heads, but I think that’s just an aesthetic one has to acquire with practice.

Where Next?

Popular in Questions Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
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
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
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

Other popular topics Top

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
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
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49266 226
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New

We're in Beta

About us Mission Statement