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

peerreynders

The fact I was nudging towards was that a keyword list can have multiple values for a single key.

However in 99.9% of the cases there is no intent to support multiple values per key - and nothing in the original post suggested that there was any intent to support multiple :a or :b values.

I suspect keyword lists being the default mechanism for passing options to Elixir functions motivated the choice here - not the fact that a keyword list can have multiple values for the same key.

At the same time I’m not aware of a convention that suggests which value should be given preference when an option appears multiple times in an option list - the one closest to the beginning of the list or the one closest to the end?(‡)

My choice would be to pass options in a Map to begin with but I suspect that the “option keyword list mechanism” was devised during a time where there were no Maps but only HashDicts (Edit: The reasons are actually quite different).

Again - in 99.9% of the cases the possibility of multiple values per key is a non-issue but it probably doesn’t hurt to keep this potential “gotcha” in mind.

‡ Based on José’s comment:

so

> Enum.into([a: 2, a: 3], %{a: 1})
%{a: 3}

the last value should likely be the “honoured” value (which interestingly could be the “first” value added to the list when it was created by cons-ing) if only one value is desired. However since something like for cmd_opts reverses the order of the options during validation, I guess, no definitive call can be made.

peerreynders

peerreynders

I’m thinking idiomatic “Keywords -> Map” conversion shouldn’t be:

iex(1)> [a: 1, b: 2, a: 3] |> Enum.into(%{})
%{a: 3, b: 2}

but

iex(2)> [a: 1, b: 2, a: 3] |> Enum.reverse() |> Enum.reduce(%{}, fn({k,v}, acc) -> (Map.update acc, k, [v], &([v|&1])) end)
%{a: [1, 3], b: [2]}

Where Next?

Popular in Questions Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
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
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
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
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
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
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
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

We're in Beta

About us Mission Statement