Secretmapper

Secretmapper

What's the most idiomatic way to require a key in a keyword list or make it optional

Hello! Elixir’s syntax is pretty and expressive, but at the same time this comes on the cost of knowing just what the best way to write something is.

I have a function:

 def my_fun(options, sup_opts)

This is what I want to achieve. I will refer to ‘body’ as the general function logic.

  1. If options has a and false, then pass options to the body
  2. If options has a and b, (and a is not false) pass options to the body
  3. If options has a and b, and a is false error out
  4. If options has a (and it’s not false), error
  5. If options has b only, error.
  6. If options has neither a nor b, error out

options in this case might have different keys in it.

This would be easy enough to do with a bunch of if statements, but I’m curious if there’s a more idiomatic way to do this through guards and function declarations.

Most Liked

NobbZ

NobbZ

My usual way is to convert Keywordlists to Maps, then use the map:

def my_fun(options, sup_opts) when is_list(options), do: options |> Enum.into(%{}) |> my_fun(sup_opts)
def my_fun(opts = %{a: false, b: _}, sup_opts), do: # this is case 3
def my_fun(opts = %{a: false}, sup_opts), do: # this is case 1
def my_fun(opts = %{a: a, b: b}, sup_opts), do: # this is case 2
def my_fun(opts = %{a: _}, sup_opts), do: # this is case 4
def my_fun(opts = %{b: _}, sup_opts), do: # this is case 5
def my_fun(opts = %{}, sup_opts), do: # this is case 6

beware of the fact, that I had to reorder your requirements in a way that previous patterns do some filtering. Here case 1 will match on :a beeing false, but it will never see a Map containing a :b as key, since those match on the previous clause for case 3, but if you swap them you will never reach case 3, because case 1 would always match.

12
Post #2
peerreynders

peerreynders

@NobbZ’s approach is superior if you are going convert the keyword list to a Map anyway (for later convenience). But I don’t see anything wrong with the “low-tech” approach if you are trying to stick with the keyword list:

defmodule Demo do

  defp my_fun_do_it(_options, _sup_opts),
    do: IO.puts "Do it!"

  def my_fun(options, sup_opts) do
    case {Keyword.fetch(options, :a), Keyword.fetch(options, :b)} do
      {{:ok, false}, {:ok, _b}} -> IO.puts "Error case 3"
      {{:ok, false}, _}         -> my_fun_do_it(options, sup_opts)
      {{:ok, _a},    {:ok, _b}} -> my_fun_do_it(options, sup_opts)
      {{:ok, _a},    _}         -> IO.puts "Error case 4"
      {:error,       {:ok, _b}} -> IO.puts "Error case 5"
      _                         -> IO.puts "Error case 6"
    end
  end
end

(Demo.my_fun [a: false], [])
(Demo.my_fun [a: nil, b: nil], [])
(Demo.my_fun [a: false, b: nil], [])
(Demo.my_fun [a: nil], [])
(Demo.my_fun [b: nil], [])
(Demo.my_fun [], [])
$ elixir demo.exs
Do it!
Do it!
Error case 3
Error case 4
Error case 5
Error case 6

Note however that there is a subtle difference in the “fetch” and “convert to Map” approach. Given that keyword lists can have multiple occurrences of the same key “fetch” will simply grab the first occurrence while the “conversion to Map” will only contain the last occurrence.

iex(1)> Keyword.fetch([a: false, a: 0],:a)
{:ok, false}
iex(2)> Keyword.fetch([a: 0, a: false],:a)
{:ok, 0}
iex(3)> [a: false, a: 0] |> Enum.into(%{}) 
%{a: 0}

Whether or not that makes a difference depends entirely on the circumstances.

NobbZ

NobbZ

Which can be easily worked around:

iex(1)> [a: false, a: 0] |> Enum.reverse |> Enum.into(%{})
%{a: false}

Last Post!

Ryzey

Ryzey

Good point. I guess you could use a custom default value e.g. :no-option that you should then consider to be equivalent whether it was provided as such or assigned as a default.

It would be good if you could explicitly pattern match for undefined values without having to match “_” first.

Edited to include quote.

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
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
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
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
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

Other popular topics Top

grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 54006 488
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31494 112
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement