Case Expressions
Case matching in Elixir gives you a few different ways to check the type of a value. Here we check if val
is a map or list.
def foo(val) do
case val do
%{} = map ->
map |> Map.keys() |> Enum.join(",")
list when is_list(list) ->
Enum.join(list, ",")
_ ->
""
end
end
Erie needs some syntactic way to allow matching in a case expression like this. One option is to compel Elixir’s match operator (=
) to work in prefix notation.
(def foo [val]
(case val
[(= %{} map) (-> map (Map.keys) (Enum.join ","))
(= [] list) (Enum.join list ",")
_ ""]))
I don’t like this (and this applies to Elixir as well) because %{}
matches on all maps, regardless of if they’re empty or not, but []
only matches on empty lists. I’d rather match on explicit Erie type names to accomplish this.
(def foo [val]
(case val
[(= Map map) …
(= List list) …
_ …]))
This could potentially work, but Erie’s union types can make it impossible for the compiler to handle. In this next example, how will the compiler know if list
is a (List String)
or a (List Integer)
? toUpper
can only operate on String
s, so the compiler should fail on the last line.
(deftype EitherList []
(union [(List String) (List Integer)]))
(sig foo [EitherList] (List String))
(def foo [val]
(case val
[(= List list) (Enum.map list toUpper)]))
Matching on the entire type name should fix that problem.
(deftype EitherList []
(union [(List String) (List Integer)]))
(sig foo [EitherList] (List String))
(def foo [val]
(case val
[(= (List String) list) (Enum.map list toUpper)
(= (List Integer) list) (Enum.map list toString)]))
What about when you want undefined polymorphic type parameters? Should this type of matching be allowed? Exploring this in the next example, the first match, on (List String)
, seems like it should be compilable because it returns the exact same type it operates on. But the second match, on (List Integer)
doesn’t seem like it should compile. It returns a different type than it matches on, and how can the compiler know if that fits the EitherList
pattern?
(deftype EitherList [a b]
(union [(List a) (List b)]))
(sig foo [(EitherList a b)] (EitherList a b))
(def foo [val]
(case val
[(= (List String) list) (Enum.map list toUpper)
(= (List Integer) list) (Enum.map list toString)
(= (List a) list) list]))
For now I’ll disallow matching on the type parameter. I’m not sure it’s even technically possible to ever accomplish so I’m happy to hold off.
That should cover matching on types, but what about values? Elixir’s ability to match on values is also worth copying.
def foo(val) do
case val do
[] -> "empty"
[x] -> "one element"
[x|_] -> "multiple elements"
end
end
I don’t really like the [x|_]
pattern because it feels too infix-y for a lisp, but I’ll leave it in the examples. It’s likely to change before making its way into Erie for good.
(sig foo [(List String)] String)
(def foo [val]
(case val
[[] "empty"
[x] "one element"
[x|_] "multiple elements"]))
This should combine well with union type matching. It’s a little dense, but I think it’s still readable.
(deftype EitherList []
(union [(List String) (List Integer)]))
(sig foo [EitherList] String)
(def foo [val]
(case val
[(= (List String) [x]) "one element string list"
(= (List Integer) []) "empty integer list"
(= (List String) list) "empty or mulitple value string list"
_ "anything else"]))
The one problem that still needs exploring is how to get the code to match on the right branch.
(deftype EitherList []
(union [(List String) (List Integer)]))
(sig foo [EitherList] String)
(def foo [val]
(case val
[(= (List String) []) "empty string list"
(= (List Integer) []) "empty integer list"
_ "anything else"])
Once this type checks, the compiled Erlang Abstract Form will know that val
is a list, but if it’s empty, there won’t be any other type information to glean from it and help with matching. I’m not sure exactly how to fix this, but it’s something to keep thinking about.