First Class Modules are like First Class Functions.
Basically, you know how you can pack a function up into a variable, ferry it around, and call it ‘later’? Like:
iex(1)> addone = fn a -> a + 1 end
#Function<7.91303403/1 in :erl_eval.expr/5>
iex(2)> addone.(1)
2
This is a First Class Function.
A First Class Module is, also likewise, a module that you can pack up into a variable, pass it around, and call it again:
iex(3)> defmodule Ops do
...(3)> def add1(a), do: a + 1
...(3)> def double(a), do: a * 2
...(3)> end
{:module, Ops,
<<70, 79, 82, 49, 0, 0, 4, 188, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 133,
0, 0, 0, 16, 10, 69, 108, 105, 120, 105, 114, 46, 79, 112, 115, 8, 95, 95,
105, 110, 102, 111, 95, 95, 7, 99, 111, ...>>, {:double, 1}}
iex(4)> blah = Ops
Ops
iex(5)> blah.add1(1)
2
iex(6)> blah.double(2)
4
And for completion, an OCaml Functor is like a Closure, but for Modules. Basically you know how Elixir can ‘close’ around it’s environment to carry it around, like this:
iex(7)> outside = "Hello "
"Hello "
iex(8)> f = fn(name) -> output + name end
** (CompileError) iex:8: undefined function output/0
(stdlib) lists.erl:1354: :lists.mapfoldl/3
(elixir) src/elixir_fn.erl:14: anonymous fn/4 in :elixir_fn.expand/3
(stdlib) lists.erl:1354: :lists.mapfoldl/3
iex(8)> f = fn(name) -> outside <> name end
#Function<7.91303403/1 in :erl_eval.expr/5>
iex(9)> f.("zachgardwood")
"Hello zachgardwood"
You see how it can ‘close’ around the environment to carry ‘new’ data that may not even exist until runtime? An OCaml Functor let’s you do the same thing with Modules. In Elixir it would have been like:
iex(1)> defmodule CondMap do
...(1)> def create(cond), do: {__MODULE__, cond, %{}}
...(1)> def get(key, {__MODULE__, _cond, map}), do: map[key]
...(1)> def set(key, value, {__MODULE__, cond, map}) do
...(1)> cond.(key, value) || throw "Invalid key and value: #{inspect {key, value}}"
...(1)> {__MODULE__, cond, Map.put(map, key, value)}
...(1)> end
...(1)> end
{:module, CondMap,
<<70, 79, 82, 49, 0, 0, 7, 148, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 215,
0, 0, 0, 25, 14, 69, 108, 105, 120, 105, 114, 46, 67, 111, 110, 100, 77, 97,
112, 8, 95, 95, 105, 110, 102, 111, 95, ...>>, {:set, 3}}
iex(2)> string_map = CondMap.create(fn(_key, value) -> is_binary(value) end)
{CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{}}
iex(3)> string_map = string_map.set(:a, "hi")
{CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{a: "hi"}}
iex(4)> string_map = string_map.set(:b, :an_atom)
** (throw) "Invalid key and value: {:b, :an_atom}"
iex:5: CondMap.set/3
iex(4)> string_map.get(:a)
"hi"
However, they encouraged the OTP group to remove tuple call support, which is what enabled Functors on the BEAM, so now if you try to do that on modern OTP versions you just get:
iex(3)> string_map.set(:a, "hi")
** (ArgumentError) you attempted to apply a function on {CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{a: "hi"}}. Modules (the first argument of apply) must always be an atom
:erlang.apply({CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{a: "hi"}}, :get, [:a])
So yeah, a useful feature was broken…
Being able to ‘close’ around a Module just like you can a Function to make a Closure is so very useful. Yet they decided to keep Closures, yet get rid of Functors…