Algae provides a boilerplate-avoiding DSL for defining algebraic data types (ADTs), plus several common structures
Quickstart
Add Algae to your list of dependencies in mix.exs
:
def deps do
[{:algae, "~> 1.2"}]
end
Table of Contents
Note
Please import Algae
before trying out the examples below.
The samples assume that is has already been done to remove
the unnecessary clutter.
Product Builder
Build a product type
Includes:
- Struct
- Type definition
- Constructor function (for piping and defaults)
- Implicit defaults for simple values
Definition DSL
For convenience, several variants of the DSL are available.
Standard
defmodule Player do
# =============== #
# Data Definition #
# =============== #
defdata do
name :: String.t()
hit_points :: non_neg_integer()
experience :: non_neg_integer()
end
# =================== #
# Rest of Module #
# (business as usual) #
# =================== #
@spec attack(t(), t()) :: {t(), t()}
def attack(%{experience: xp} = player, %{hit_points: hp} = target) do
{
%{player | experience: xp + 50},
%{target | hit_points: hp - 10}
}
end
end
#=> %Player{name: "Sir Bob", hit_points: 10, experience: 500}
Single Field Shorthand
Without any fields specified, Algae will default to a single field with the same name as the module (essentially a "wrapper type"). You must still provide the type for this field, however.
Embedded in another module:
defmodule Id do
defdata any()
end
%Id{}
#=> %Id{id: nil}
Standalone:
defdata Wrapper :: any()
%Wrapper{}
#=> %Wrapper{wrapper: nil}
Constructor
A helper function, especially useful for piping. The order of arguments is the same as the order that they are defined in.
defmodule Person do
defdata do
name :: String.t()
age :: non_neg_integer()
end
end
Person.new("Rachel Weintraub")
#=> %Person{
# name: "Rachel Weintraub",
# age: 0
# }
Constructor Defaults
Fields will automatically default to a sensible value (a typical "zero" for
that datatype). For example, non_neg_integer()
will default to 0
,
and String.t()
will default to ""
.
You may also overwrite these defaults with the \\
syntax.
defmodule Pet do
defdata do
name :: String.t()
leg_count :: non_neg_integer() \\ 4
end
end
Pet.new("Crookshanks")
#=> %Pet{
# name: "Crookshanks",
# leg_count: 4
# }
Pet.new("Paul the Psychic Octopus", 8)
#=> %Pet{
# name: "Paul the Psychic Octopus",
# leg_count: 8
# }
This overwriting syntax is required for complex types:
defdata Grocery do
item :: {String.t(), integer(), boolean()} \\ {"Orange", 4, false}
end
Grocery.new()
#=> %Grocery{
# item: {"Orange", 4, false}
# }
Overwrite Constructor
The new
constructor function may be overwritten.
defmodule Constant do
defdata :: fun()
def new(value), do: %Constant{constant: fn _ -> value end}
end
fourty_two = Constant.new(42)
fourty_two.constant.(33)
#=> 42
Empty Tag
An empty type (with no fields) is definable using the none
() type
defmodule Nothing do
defdata none()
end
Nothing.new()
#=> %Nothing{}
Sum Builder
Build a sum (coproduct) type from product types
defmodule Light do
# ============== #
# Sum Definition #
# ============== #
defsum do
defdata Red :: none()
defdata Yellow :: none()
defdata Green :: none()
end
# =================== #
# Rest of Module #
# (business as usual) #
# =================== #
def from_number(1), do: %Light.Red{}
def from_number(2), do: %Light.Yellow{}
def from_number(3), do: %Light.Green{}
end
Light.new()
#=> %Light.Red{}
Embedded Products
Data with multiple fields can be defined directly as part of a sum
defmodule Pet do
defsum do
defdata Cat do
name :: String.t()
claw_sharpness :: String.t()
end
defdata Dog do
name :: String.t()
bark_loudness :: non_neg_integer()
end
end
end
Default Constructor
The first defdata
's constructor will be the default constructor for the sum
defmodule Maybe do
defsum do
defdata Nothing :: none()
defdata Just :: any()
end
end
Maybe.new()
#=> %Maybe.Nothing{}
Tagged Unions
Sums join existing types with tags: new types to help distinguish the context that they are in (the sum type)
defdata Book :: String.t() \\ "War and Peace"
defdata Video :: String.t() \\ "2001: A Space Odyssey"
defmodule Media do
defsum do
defdata Paper :: Book.t()
defdata Film :: Video.t() \\ Video.new("A Clockwork Orange")
end
end
media = Media.new()
#=> %Paper{
# paper: %Book{
# book: "War and Peace"
# }
# }
A Sampling of ADTs
See complete docs for more
Algae.Id
The simplest ADT: a simple wrapper for some data
%Algae.Id{id: "hi!"}
Algae.Maybe
Maybe represents the presence or absence of something.
Please note that nil
is actually a value, as it can be passed to functions!
nil
is not bottom!
Algae.Maybe.new()
#=> %Algae.Maybe.Nothing{}
Algae.Maybe.new(42)
#=> %Algae.Maybe.Just{just: 42}
Tree.BinarySearch
alias Algae.Tree.BinarySearch, as: BTree
# 42
# / \
# 77 1234
# / \
# 98 32
BTree.Branch.new(
42,
BTree.Branch.new(77),
BTree.Branch.new(
1234,
BTree.Branch.new(98),
BTree.Branch.new(32)
)
)
#=> %Algae.Tree.BinarySearch.Branch{
# value: 42,
# left: %Algae.Tree.BinarySearch.Branch{
# value: 77,
# left: %Algae.Tree.BinarySearch.Empty{},
# right: %Algae.Tree.BinarySearch.Empty{}
# },
# right: %Algae.Tree.BinarySearch.Branch{
# value: 1234,
# left: %Algae.Tree.BinarySearch.Branch{
# value: 98,
# left: %Algae.Tree.BinarySearch.Empty{},
# right: %Algae.Tree.BinarySearch.Empty{}
# },
# right: %Algae.Tree.BinarySearch.Branch{
# value: 32,
# left: %Algae.Tree.BinarySearch.Empty{},
# right: %Algae.Tree.BinarySearch.Empty{}
# }
# }
# }