117 lines
3.4 KiB
Org Mode
117 lines
3.4 KiB
Org Mode
:PROPERTIES:
|
|
:ID: b413e4db-1367-4936-8a46-cd5b86178e29
|
|
:END:
|
|
#+title: Either in Clojure
|
|
#+author: Yann Esposito
|
|
#+date: [2022-08-15]
|
|
|
|
- tags ::
|
|
- source ::
|
|
|
|
* Either Either or Exceptions?
|
|
|
|
When you start working with Clojure (or many other programming language) a
|
|
classic notion of error is, for your function, to return nothing is an error
|
|
occurred.
|
|
But quite often you want to convey more detail about the error so you could act
|
|
differently depending on it.
|
|
Or provide a more detailed error message to your end user, etc…
|
|
|
|
The recommended method to handle errors in Clojure (and a lot of other
|
|
programming languages) is to throw exceptions.
|
|
While this is perfectly pragmatic and reasonable, exceptions have a few
|
|
undesirable properties.
|
|
|
|
First, they are impure, which somehow break the principles behind functional
|
|
programming.
|
|
So if you are using Clojure chance are you prefer to keep it as pure as possible.
|
|
|
|
Second, exceptions are generally expensive, and should probably be avoided if
|
|
possible.
|
|
(*TODO*: write a short benchmark).
|
|
|
|
Third, Exceptions lack of composability properties and are often hidden.
|
|
It is generally impossible to know if a function could or could not throw an
|
|
exception.
|
|
And also if you should or should not do something about it.
|
|
Also, while possible it is not very natural to accumulate errors generated by a
|
|
sequence of actions.
|
|
The same could be said if you want to transform the error depending on the code
|
|
context.
|
|
Here are two example of code:
|
|
|
|
#+begin_src clojure
|
|
;; Example of accumulating values and errors
|
|
(for [x things]
|
|
(try (do-some-action %)
|
|
(catch Exception e
|
|
(do-something-with-exception e))))
|
|
|
|
;; Example of tranforming value
|
|
(try (do-some-action x)
|
|
(catch Exception e
|
|
(transform e)))
|
|
#+end_src
|
|
|
|
We see this is easy, but, I don't know, it looks a bit cumbersome to me.
|
|
|
|
So what is the other possibility?
|
|
|
|
In the Statically Typed Pure Function Programming world people often use ~Either~.
|
|
What is this?
|
|
Mainly this is a type that represent the notion of: "We have either something or
|
|
an error."
|
|
|
|
Either contains two component, by convention a left and a right. And also by
|
|
convention the right contain the value while the left contain the error.
|
|
Why? Because right also means correct in English.
|
|
|
|
|
|
So ~Either~ is a sum type.
|
|
Sum types is probably THE feature I miss the most in many programming languages.
|
|
Anyway, it is not difficult to simulate a sum type.
|
|
Here are a few possible representations:
|
|
|
|
Using a tag field that indicate if the content is a right or a left:
|
|
|
|
#+begin_src clojure
|
|
{:type (enum :left :right)
|
|
:content value-or-error}
|
|
|
|
;; example for (Right 42)
|
|
{:type :right
|
|
:content 42}
|
|
;; exaple for (Left "error")
|
|
{:type :left
|
|
:content "error"}
|
|
#+end_src
|
|
|
|
Another way to prevent some potential issue with conflicting different schemas
|
|
(imagine if you had to save the previous representation within Elasticsearch for
|
|
example, the mapping would probably be difficult to write).
|
|
|
|
#+begin_src clojure
|
|
{:type (enum :left :right)
|
|
(optional-key :left) Error
|
|
(optional-key :right) Value}
|
|
;; example for (Right 42)
|
|
{:type :right
|
|
:right 42}
|
|
;; exaple for (Left "error")
|
|
{:type :left
|
|
:left "error"}
|
|
#+end_src
|
|
|
|
And the last one not as generic, but good enough for our use case, both terser,
|
|
easier to read and manipulate:
|
|
|
|
#+begin_src clojure
|
|
{(optional-key val): Value
|
|
(optional-key err): Error}
|
|
;; example for (Right 42)
|
|
{:val 42}
|
|
;; example for (Left "error")
|
|
{:err "error"}
|
|
#+end_src
|
|
|
|
So we'll choose this one.
|