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.

mudasobwa

mudasobwa

Creator of Cure

It won’t work, unfortunately. nil && default would result in nil.

E.g. the logger. nil is none, while default is :console.

Where Next?

Popular in Questions Top

Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
hpopp
To simplify some tasks at work, I wrote and published this package yesterday. It’s a simple macro that enables Access behaviour on struct...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
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 42633 214
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New

We're in Beta

About us Mission Statement