Defining Named Functions with a List Comprehension
Aug 19 2016
I was perusing an Elixir project1 a few months ago, and I came across a really cool pattern. In Elixir, you can define named functions using a list comprehension.
defmodule MyModule do
for n <- 1..5 do
def match_me(unquote(n)), do: "I matched to #{unquote(n)}"
end
def match_me(_n) do
"I did not match"
end
end
In the example above, for each number between 1
and 5
(inclusive), a function is generated with the current value of n
as the argument. If we were to expand this out, we would have a module that looks like this.
defmodule MyModule do
def match_me(1), do: "I matched to 1"
def match_me(2), do: "I matched to 2"
def match_me(3), do: "I matched to 3"
def match_me(4), do: "I matched to 4"
def match_me(5), do: "I matched to 5"
def match_me(_n) do
"I did not match"
end
end
In this context the for
macro is working like defmacro
and operating on the code as data. Notice, that n
had to be unquoted within the function definition. Had n
not been unquoted, we would have been left with this.
defmodule MyModule do
def match_me(n), do: "I matched to #{n}"
def match_me(n), do: "I matched to #{n}"
def match_me(n), do: "I matched to #{n}"
def match_me(n), do: "I matched to #{n}"
def match_me(n), do: "I matched to #{n}"
def match_me(_n) do
"I did not match"
end
end
Of course this would always match to the first version of match_me
and always return "I match to <insert value of n>"
, leaving the last clause of match_me
to never be matched.
Application
Examples are fine for demonstration, but it is good to see where this pattern might be useful in the wild. One particular place I like to use this pattern is when I am testing a function with a range of inputs and outputs. This comes up in the prime factors kata and the roman numerals kata.
Here is an example of this pattern used for the roman numerals kata2.
defmodule RomanNumeralsTest do
use ExUnit.Case
doctest RomanNumerals
import RomanNumerals
@roman_to_arabic [
{"I", 1},
{"II", 2},
{"III", 3},
{"IV", 4},
{"V", 5},
{"VI", 6},
{"VII", 7},
{"VIII", 8},
{"IX", 9},
{"X", 10},
{"XI", 11},
{"XIV", 14},
{"XIX", 19},
{"XXVIII", 28},
{"XXXXIV", 44},
{"L", 50},
]
for {roman, arabic} <- @roman_to_arabic do
test "it returns #{roman} when given #{arabic}" do
assert convert(unquote(arabic)) == unquote(roman)
end
end
end
Drawbacks
I think this is a pretty cool feature of the language, but might be best to use with caution. The pattern has the potential introduce opacity to your code.
In my opinion, the test example above is clear enough. The @roman_to_arabic
map expresses most of what the tests needs to communicate.
My pair and I also attempted to use this pattern within the RomanNumerals
module to a handful of the function clauses in the name of removing duplication.
Here is that module.
defmodule RomanNumerals do
@numerals_map [
{"M", 1000},
{"D", 500},
{"C", 100},
{"L", 50},
{"X", 10},
{"V", 5},
{"I", 1}
]
def convert(0), do: ""
def convert(4), do: "IV"
def convert(9), do: "IX"
for {roman_numeral, arabic} <- @numerals_map do
def convert(number) when number >= unquote(arabic) do
unquote(roman_numeral) <> convert(number - unquote(arabic))
end
end
end
I find that this does not do as great of job of communicating intent. Not terrible, but not great either.
Wrap up
Use with care.
1. Unfortunately the name of the project has escaped me
2. We stopped at 50
Recent Articles
- Apprenticeship Retro Oct 14 2016
- What the hell is CORS Oct 13 2016
- Cross-site Tracing Oct 11 2016
- Learning Testing Tools First Oct 7 2016