benhoven

benhoven

dealing with optional arguments via keyword list - my own take

Hi Everybody,

I spent some time testing and trying different ways how to pass, process and use optional opts in a function argument.

Ideally I’d like to settle on a single piece of code - something I can use in all my functions.

By searching I quickly realized that everybody has own take on this.

At the end I developed my own way and I’d like to kindly ask you if somebody can review it advise me if this is clever or stupid and I shouldn’t use it ;-).

defmodule Helper.ArgOpts do
  @spec add_argument(map, atom, keyword, atom) :: map
  def add_argument(map, map_key, keyword, keyword_key)
      when is_map(map) and is_atom(map_key) and is_list(keyword) and is_atom(keyword_key) do
    values =
      if Keyword.has_key?(keyword, keyword_key),
        do: Keyword.get_values(keyword, keyword_key),
        else: []

    Map.put(map, map_key, values)
  end
end

defmodule MySuperApp do
  @opts_default color: :red,
           engine: :standard
  @spec make_bike(atom, list) :: String.t()
  def make_bike(brand, opts \\ []) when is_atom(brand) and is_list(opts) do
    opts = @opts_default ++ opts
    all_opts = Enum.into(opts, %{})

	# stupid example how to use the `opts` args:
    "bike: #{all_opts.color} #{brand} with #{all_opts.engine} engine"
  end

  @opts_default number_of_wheels: 4,
           color: :red,
           engine_size: :standard,
           tuning?: false
  @spec make_car(atom, list) :: String.t()
  def make_car(brand, opts \\ []) when is_atom(brand) and is_list(opts) do
    opts = @opts_default ++ opts
    all_opts = Enum.into(opts, %{})
    all_opts = Helper.ArgOpts.add_argument(all_opts, :features, opts, :feature)

	# stupid example how to use the `opts` args:
    engine =
      if all_opts.tuning?,
        do: "TUNED engine",
        else: "#{all_opts.engine_size} engine"

    """
    car: #{all_opts.color} #{brand}
      with #{all_opts.number_of_wheels} wheels
      and #{engine}
      and features: #{inspect(all_opts.features)}
    """
  end
end

IO.puts(MySuperApp.make_bike(:honda))

IO.puts(MySuperApp.make_bike(:kawasaki, engine: :fastest, color: :green))

IO.puts(MySuperApp.make_car(:ferrari))

IO.puts(MySuperApp.make_car(:lamborghini, color: :yellow, feature: :air_con, feature: :shiny_wheels))

IO.puts(MySuperApp.make_car(:hummer, color: :black, number_of_wheels: 6, tuning?: true, feature: :dark_window_tint))

Basically the idea is to have an attribute @opts_default with default values on top of each function that has opts argument.

In a simple functions (like make_bike/2) the Keyword list with defaults + the keyword list with user specified values is converted into a Map. Defaults get overwritten by user specified values.

In more complicated functions (like make_car/2) it is possible to repeat a keyword (in the example feature) and then add a features key into the new map. New features key holds all values (or empty list if feature is not specified).

Can I please get feedback if this is/isn’t a good way.

Thank you ;-).

Kind regards,

Ben

Most Liked

mudasobwa

mudasobwa

Creator of Cure

Keyword.get/3 accepts a default value, which would work for false and nil values, while your version would (surprisingly) overwrite them.

color = Keyword.get(opts, :color, @default_opts[:color])

Example:

opts = {color: false}

opts[:color] || true
#⇒ true

Keyword.get(opts, :color, true)
#⇒ false
krstfk

krstfk

I am confused as to why you wouldn’t use the Keyword module and its functions for that purpose.

You can easily merge the options provided by the user and the defaults with Keyword.merge/2 eg :

opts = Keyword.merge(@default_opts, opts)

You can also gather duplicated keys with Keyword.get_values/2 or, closer to your example Keyword.pop_values/2 eg :

 @opts_default number_of_wheels: 4,
           color: :red,
           engine_size: :standard,
           tuning?: false
  @spec make_car(atom, list) :: String.t()
  def make_car(brand, opts \\ []) when is_atom(brand) and is_list(opts) do
    opts = Keyword.merge(@opts_default, opts)
    {features, opts } = Keyword.pop_values(opts, :feature)
    all_opts = Enum.into(opts, %{}) |> Map.put(:features, features)

	# stupid example how to use the `opts` args:
    engine =
      if all_opts.tuning?,
        do: "TUNED engine",
        else: "#{all_opts.engine_size} engine"

    """
    car: #{all_opts.color} #{brand}
      with #{all_opts.number_of_wheels} wheels
      and #{engine}
      and features: #{inspect(all_opts.features)}
    """
  end
derek-zhou

derek-zhou

I usually just do:

color = opts[:color] || @default_opts[:color]

for every single opt that I care. it is the same as Keyword.merge/2 but explicit, you can add any sanity check you want, and you end up with local bindings that are easier to use.

Where Next?

Popular in Questions Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
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
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
dotdotdotPaul
Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’...
New

Other popular topics Top

New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41989 114
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43757 214
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement