Fernando Hamasaki de Amorim

Prodis' blog

Miss Elixir

5 October 2020

Miss Elixir library brings in a non-intrusive way some extra functions that, for different reasons, are not part of the Elixir standard library.

Motivation

After almost two years working full-time developing Elixir applications, I miss some functions in Elixir modules like Kernel, Map and String.

Inevitably my team and I created some “infamous util modules”, that in general each big application has, to fill the lack of some functions.

Of course you might be thinking now: “Oh man! One more library with utility functions…”. And maybe you are right, or maybe not. :)

Said that, I kindly dare you to take a look in the examples below and see some real code where the functions in Miss Elixir were inspired. And after that, you make your conclusions if some of those functions maybe should be part of the Elixir standard library in the future versions.

Design decisions

Before jumping to the examples, I just want to make clear some design decisions in Miss Elixir:

Examples

Miss.String

In Erlang and Elixir concatenating binaries will copy the concatenated binaries into a new binary. Every time you concatenate binaries (<>) or use interpolation (#{}) you are making copies of those binaries.

To build a string, it is cheaper and more efficient to use IO lists to build the binary just once instead of concatenating along the way.

See the Elixir IO Data documentation for more information.

In the example below, the private function build_href/3 uses IO.iodata_to_binary/1 to build a string from four pieces of other strings and one integer value:

defmodule LinkDecorator do
  ...

  @spec decorate(Link.t(), String.t(), integer()) :: Link.t()
  def decorate(%Link{type: :book} = link, url, link_id) do
    %{
      link
      | method: "POST",
        href: build_href(url, "book", link_id)
    }
  end

  def decorate(%Link{type: :pre_book} = link, url, link_id) do
    %{
      link
      | method: "GET",
        href: build_href(url, "pre-book", link_id)
    }
  end

  def decorate(%Link{} = link, _url, _link_id), do: link

  @spec build_href(String.t(), String.t(), integer()) :: String.t()
  defp build_href(url, path, link_id),
    do: IO.iodata_to_binary([url, "/", path, "?link_id=", link_id])

  ....
end

The same code using Miss.String.build/1 becomes more clear and explicit you are building a string:

defmodule LinkDecorator do
  ...

  @spec build_href(String.t(), String.t(), integer()) :: String.t()
  defp build_href(url, path, link_id),
    do: Miss.String.build([url, "/", path, "?link_id=", link_id])

  ...
end

Or using Miss.String.build/5 (or the other function overloads with less paramters) when you have the control of the quantity of parameters to build:

defmodule LinkDecorator do
  ...

  @spec build_href(String.t(), String.t(), integer()) :: String.t()
  defp build_href(url, path, link_id),
    do: Miss.String.build(url, "/", path, "?link_id=", link_id)

  ...
end

Another case is replacing the use of Enum.join/2 (that internally uses IO.iodata_to_binary/1) to join strings. See the implementation of the private function build_holder_name/1 in the module below:

defmodule PaymentParams do
  ...

  @spec build(Payment.t()) :: map()
  def build(%Payment{
         billing: billing,
         credit_card: %CreditCard{
           expiration_month: expiration_month,
           expiration_year: expiration_year,
           transaction_id: transaction_id,
           type: type
         }
       }) do
    %{
      card: %{
        brand: CreditCardType.brand(type),
        expiration_month: expiration_month,
        expiration_year: expiration_year,
        holder_name: build_holder_name(billing),
        token: transaction_id
      },
      type: :card
    }
  end

  @spec build_holder_name(Billing.t()) :: String.t()
  defp build_holder_name(%Billing{first_name: first_name, last_name: last_name}),
    do: Enum.join([first_name, last_name], " ")

  ...
end

Now using Miss.String.build/3:

defmodule PaymentParams do
  ...

  @spec build_holder_name(Billing.t()) :: String.t()
  defp build_holder_name(%Billing{first_name: first_name, last_name: last_name}),
    do: Miss.String.build(first_name, " ", last_name)

  ...
end

Miss.Kernel

Imagine you are piping some operations to build fields to create a struct. Using Kernel.struct/2 it is necessary to assign the map to a variable before creating the struct:

defmodule PreBookParams do
  ...

  defstruct [...]

  @spec build(Params.t(), Hotel.t(), Offer.t()) :: t()
  def build(%Params{} = params, %Hotel{} = hotel, %Offer{} = offer) do
    attrs =
      %{
        check_out: to_string(params.check_out),
        check_in: to_string(params.check_in),
        currency: params.currency_code,
        hotel_id: hotel.id,
        locale: params.language_code,
        rooms: RoomConfiguration.to_raw_rooms(params.rooms),
        user_ip: params.customer_ip
      }
      |> Map.merge(build_price(offer, params.currency_code))

    struct(__MODULE__, attrs)
  end

  ...
end

Using Miss.Kernel.struct_inverse/2 the map can be piped when creating the struct:

defmodule PreBookParams do
  ...

  defstruct [...]

  @spec build(Params.t(), Hotel.t(), Offer.t()) :: t()
  def build(%Params{} = params, %Hotel{} = hotel, %Offer{} = offer) do
    %{
      check_out: to_string(params.check_out),
      check_in: to_string(params.check_in),
      currency: params.currency_code,
      hotel_id: hotel.id,
      locale: params.language_code,
      rooms: RoomConfiguration.to_raw_rooms(params.rooms),
      user_ip: params.customer_ip
    }
    |> Map.merge(build_price(offer, params.currency_code))
    |> Miss.Kernel.struct_inverse(__MODULE__)
  end

  ...
end

If you need to create a list of structs from an Enumerable, Miss.Kernel.struct_list/2 does this job for you.

defmodule User do
  defstruct name: "User"
end

# Using a list of maps
iex> Miss.Kernel.struct_list(User, [
...>   %{name: "Akira"},
...>   %{name: "Fernando"}
...> ])
[
  %User{name: "Akira"},
  %User{name: "Fernando"}
]

# Using a list of keywords
iex> Miss.Kernel.struct_list(User, [
...>   [name: "Akira"],
...>   [name: "Fernando"]
...> ])
[
  %User{name: "Akira"},
  %User{name: "Fernando"}
]

Elixir Kernel has div/2 to perform an integer division and rem/2 to compute the remainder of an integer division, but misses a function to do both at the same time.

Miss.Kernel.div_rem/2 to the rescue:

iex> Miss.Kernel.div_rem(5, 2)
{2, 1}

Miss.Map

The next code example was not extracted from a real application, but it is clear and didactic enough to show how to convert a struct to a map going through all nested structs using Miss.Map.from_nested_struct/2.

Given the following Post struct with author and comments:

defmodule Post do
  defstruct [:title, :text, :date, :author, comments: []]
end

defmodule Author do
  defstruct [:id, :name]
end

defmodule Comment do
  defstruct [:text]
end

post = %Post{
  title: "My post",
  text: "Something really interesting",
  date: ~D[2010-09-01],
  author: %Author{
    id: 1234,
    name: "Pedro Bonamides"
  },
  comments: [
    %Comment{text: "Comment one"},
    %Comment{text: "Comment two"}
  ]
}

Using Map.from_struct/1 only the root struct will be converted:

iex> Map.from_struct(post)
%{
  title: "My post",
  text: "Something really interesting",
  date: ~D[2010-09-01],
  author: %Author{
    id: 1234,
    name: "Pedro Bonamides"
  },
  comments: [
    %Comment{text: "Comment one"},
    %Comment{text: "Comment two"}
  ]
}

Using Miss.Map.from_nested_struct/2 all the nested structs are converted:

iex> Miss.Map.from_nested_struct(post)
%{
  title: "My post",
  text: "Something really interesting",
  date: %{
    calendar: Calendar.ISO,
    day: 1,
    month: 9,
    year: 2010
  },
  author: %{
    id: 1234,
    name: "Pedro Bonamides"
  },
  comments: [
    %{text: "Comment one"},
    %{text: "Comment two"}
  ]
}

But wait, there is something weird. The Date struct was also converted to a map, that it is something not much useful.

To solve this situation, you can skip the conversion of any struct.

iex> Miss.Map.from_nested_struct(post, [{Date, :skip}])
%{
  title: "My post",
  text: "Something really interesting",
  date: ~D[2010-09-01],
  author: %{
    id: 1234,
    name: "Pedro Bonamides"
  },
  comments: [
    %{text: "Comment one"},
    %{text: "Comment two"}
  ]
}

Or you can provide a function to transform a struct instead of converting to a map:

iex> Miss.Map.from_nested_struct(post, [
...>   {Date, &to_string/1},
...>   {Comment, fn %Comment{text: text} -> text end}
...> ])
%{
  title: "My post",
  text: "Something really interesting",
  date: "2010-09-01",
  author: %{
    id: 1234,
    name: "Pedro Bonamides"
  },
  comments: [
    "Comment one",
    "Comment two"
  ]
}

Miss.List

Last but not least, Miss.List.intersection/2 returns a list containing only the elements in common of the two given lists.

iex> Miss.List.intersection([1, 2, 3, 4, 5], [3, 4, 5, 6, 7])
[3, 4, 5]

Conclusion

I hope some of the functions in Miss Elixir can be helpful to your Elixir applications as they are for mine.

There are more a couple of functions not mentioned in this post. Check the full documentation.

And how about you, what function do you miss in Elixir? Of course, contributions, suggestions and comments are welcome. :)


elixir, miss, library, package