🗓️Day 21

Sunday, 28 August 2022 - Day 21 of Exhort August 2022 - Exercism (My Elixir Journey)

Exercises

Top Secret

Exercise on Exercism | View my solution

Solution

Expand to see code (spoiler alert)
defmodule TopSecret do
  @spec to_ast(String.t()) :: tuple()
  def to_ast(string), do: Code.string_to_quoted!(string)

  @spec decode_secret_message_part(tuple(), list()) :: tuple()
  def decode_secret_message_part(ast, acc) when elem(ast, 0) in [:def, :defp] do
    ast
    |> elem(2)
    |> Enum.at(0)
    |> get_function_ast()
    |> get_function_name_and_arity()
    |> get_sliced_name()
    |> get_secret_message_part(ast, acc)
  end

  def decode_secret_message_part(ast, acc), do: {ast, acc}

  defp get_secret_message_part(name, ast, acc), do: {ast, [name | acc]}

  defp get_function_ast(ast) when elem(ast, 0) == :when do
    ast
    |> elem(2)
    |> Enum.at(0)
  end

  defp get_function_ast(ast), do: ast

  defp get_function_name_and_arity(function_ast) do
    fname =
      function_ast
      |> elem(0)
      |> Atom.to_string()

    {fname, get_arity(function_ast)}
  end

  defp get_arity(function_ast) when elem(function_ast, 2) == nil, do: -1

  defp get_arity(function_ast) do
    function_ast
    |> elem(2)
    |> Enum.count()
    |> Kernel.-(1)
  end

  defp get_sliced_name({_fname, arity}) when arity == -1, do: ""
  defp get_sliced_name({fname, arity}) when arity > -1, do: fname |> String.slice(0..arity)

  @spec decode_secret_message(String.t()) :: String.t()
  def decode_secret_message(string) do
    string
    |> to_ast()
    |> Macro.postwalk([], &decode_secret_message_part/2)
    |> Tuple.to_list()
    |> Enum.at(1)
    |> Enum.reverse()
    |> Enum.join()
  end
end

View gist on GitHub

Notes

I struggled with this one a lot. My first version of the code was not idiomatic at all. It is too verbose and relies a lot on Enum.at and elem. That's in comparison to using pattern matching.

In my second iteration I've borrowed some improvements from others. Namely, checking if the expression is a function with a in [:def, :defp] which I was checking using a conditional statement before.

But I still decided to keep my code instead of using pattern matching. Because of the complexity of the AST data structure, the pattern matching is rather complicated and I feel it's not as readable. Despite it being shorter. But this is really just a preference. Most consider the solutions that rely on pattern matching heavily more elegant.

I have also learned about mix_test_watch | Hex and I have started using it. It helps me run all tests on every save. I install the dependency:

# mix.exs (v1.13)
def deps do
  [
    {:mix_test_watch, "~> 1.0", only: :dev}
  ]
end

I configure it in my project:

# config/config.exs
import Config

if config_env() == :dev do
  config :mix_test_watch,
    clear: true
end

Then I start the watcher in iTerm:

mix test.watch --seed 0 --max-failures 1 --include pending

I have VSCode and iTerm side by side and I work on my code. On every save, the tests run in iTerm. I have reduced using iex dramatically as I am adding IO.inspect statements that immediatelly show up in iTerm after each save.

Overall progress

Last updated