# DiscUnion
## Description
Discriminated unions for Elixir.
Allows for building data structure with a closed set of representations/cases as an alternative for a tuple+atom combo.
Provides macros and functions for creating and matching on datastructres which throw compile-time and run-time
exceptions if an unknow case was used or not all cases were covered in a match. It's inspired by ML/OCaml/F# way of
building discriminated unions. Unfortunately, Elixir does not support such a strong typing and this library will not
solve this. This library allows to easly catch common mistakes at compile-time instead of run-time (those can be
sometimes hard to detect).
## How to use
In `example` folder, there is a tennis kata example, a simple coding excercise, that shows exactly how to use this
library.
To define a discriminated union, `defunion` macro is used:
``` elixir
defmodule Shape do
use DiscUnion
defunion Point
| Circle in float()
| Rectangle in any * any
end
```
Type specs in `Circle` or `Rectangle` definitions are only for description and have no influance on code nor are they
used for any type checking - there is no typchecking other then checking if correct cases were used!
When constructing a case (an union tag), you have three options:
* `from/1` macro (compile-time checking),
* `from!/` or `from!/2` functions (only run-time checking).
* a dynamicaly built macro named after union tag (in a camalized form, i.e. `Score`'s `Advantage` case, in tennis kata,
would be available as `Score.advantage/2` macro and also with compile-time checking),
If you would do `use DiscUnion, dyn_constructors: false`, dynamic constructos would not be built.
If `Score.from {Pointz, 1, 2}` be placed somwhere in `run_test_match/0` function, in tennis kata, compiler would throw
this error:
``` elixir
== Compilation error on file example/tennis_kata.exs ==
** (UndefinedUnionCaseError) undefined union case: {Pointz, 1, 2}
(disc_union) expanding macro: Score.from/1
(disc_union) example/tennis_kata.exs:38: Tennis.run_test_match/0
```
If you would use `from!/1`, this error would be thrown at run-time, or, in the case of `from!/2`, not at all! Function
`from!/2` returns it's second argument when unknow clause is passed to the function.
For each discriminated union, a special `case` macro is created. This macro checks if all cases were covered in it's
clauses (at compile-time) and expects it's predicate to be evaluated to this discriminated union's struct (checked at
run-time).
If `Game in _`, in `Tennis.score_point/2` functions, would be commented, compiler would throw this error:
``` elixir
== Compilation error on file example/tennis_kata.exs ==
** (MissingUnionCaseError) not all defined union cases are used, should be all of: Points in "PlayerPoints" * "PlayerPoints", Advantage in "Player", Deuce, Game in "Player"
(disc_union) expanding macro: Score.case/2
(disc_union) example/tennis_kata.exs:64: Tennis.score_point/2
```
You can also use a catch-all statment (_), like in a regular `case` macro (`Kernel.SpecialForms.case/2`), but here, it
needs to be explicitly enabled by passing `allow_underscore: true` option to the macro:
``` elixir
Score.case score, allow_underscore: true do
Points in PlayerPoints.forty, PlayerPoints.forty -> Score.duce
_ -> score
end
```
Otherwise you would see a smillar error like above.
## How it works
Underneath, it's just a module containg a struct with tuples and some dynamicly built macros. This property can be used
for matching in function deffinitions, altough it will not look as clearly as a `case` macro built for a discriminated
union.
The `Shape` union creates a `%Shape{}` struct with current active case held in `case` field and all possible
cases can be get by `Shape.__union_cases__/0` function:
``` elixir
%Shape{case: Point} = Shape.point
%Shape{case: {Circle, :foo}} = Shape.circle(:foo)
```
Cases that have arguments are just tuples; *n*-argument union case is a *n+1*-tuple with a case tag as it's first element.
This should work seamlessly with existing convections:
``` elixir
defmodule Result do
use DiscUnion
defunion :ok in any | :error in String.t
end
defmodule Test do
require Result
def run(file) do
res = Result.from! File.open(file)
Result.case res do
r={:ok, io_dev} -> {:yey, r, io_dev}
:error in reason when reason==:eacces -> :too_much_protections
:error in :enoent -> :why_no_file
:error in _reason -> :ney
end
end
end
```
Since cases are just a tuples, they can be used also used as a clause for `case` macro. Matching and gaurds also works!
### Side note
It is possible to place discriminated union's constructor macros in function definition:
``` elixir
defmodule ShapeArea do
require Shape
def calc_area(Shape.point), do: 0
def calc_area(Shape.circle(r)), do: :math.pi*r*r
end
```
And even use natural Elixir's multi-fun capability, to build logic, like in `score_point/2` function in tennis kata
example. Although, placing constructors inside of function definition is not a bad thing, and being able to do so is a
clear WIN! Using this technique to build business logic, instead of using discriminated union's `case` macro, is not
encouraged because nothing checks if all union cases were covered.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
1. Add disc_union to your list of dependencies in `mix.exs`:
def deps do
[{:disc_union, "~> 0.1.0"}]
end
2. Ensure disc_union is started before your application:
def application do
[applications: [:disc_union]]
end