Elixir
  • My Elixir journey
  • Why functional programming?
  • Exhort
    • 🗓️Day 22
    • 🗓️Day 21
    • 🗓️Day 20
    • 🗓️Day 19
    • 🗓️Day 18
    • 🗓️Day 17
    • 🗓️Day 16
    • 🗓️Day 15
    • 🗓️Day 14
  • Bits and pieces
    • Clean mix dependencies
    • Run tests automatically on save
    • Run tests and stop on first failure
    • How to remove Tailwind from a Phoenix project
Powered by GitBook
On this page
  • Exercises
  • Top Secret
  • Overall progress
  1. Exhort

Day 21

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

PreviousDay 22NextDay 20

Last updated 2 years ago

Exercises

Top Secret

|

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

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 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

🗓️
Exercise on Exercism
View my solution
View gist on GitHub
mix_test_watch | Hex
Top Secret
Progress
An image showing my progress on Exercism. It's 22.4% as of Sunday, 28 August 2022.