Elixir macro utilities to make type-based programming easier.
- Add
:croma
as a mix dependency. - Run
$ mix deps.get
. - Add
use Croma
in your source file to import/require macros defined in croma. - Hack!
-
Croma.Result.t(a)
is defined as@type t(a) :: {:ok, a} | {:error, any}
, representing a result of computation that can fail. -
This data type is prevalent in Erlang and Elixir world. Croma makes it easier to work with
Croma.Result.t(a)
by providing utilities such asget/2
,get!/1
,map/2
,map_error/2
,bind/2
andsequence/1
. -
You can also use Haskell-like do-notation to combine results of multiple computations by
m/1
macro. For example,Croma.Result.m do x <- {:ok, 1} y <- {:ok, 2} pure x + y end
is converted to
Croma.Result.bind(mx, fn x -> Croma.Result.bind(my, fn y -> Croma.Result.pure(x + y) end) end)
and is evaluated to
{:ok, 3}
. (The do-notation is implemented byCroma.Monad
.)
-
Annotating functions with type specifications is good but sometimes it's a bit tedious since one has to repeat some tokens (names of function and arguments, etc.) in
@spec
anddef
. -
defun/2
macro provides shorthand syntax for defining function with its typespec at once.-
Example 1
use Croma defun f(a :: integer, b :: String.t) :: String.t do "#{a} #{b}" end
is expanded to
@spec f(integer, String.t) :: String.t def f(a, b) do "#{a} #{b}" end
-
Example 2 (multi-clause syntax)
use Croma defun dumbmap(as :: [a], f :: (a -> b)) :: [b] when a: term, b: term do ([] , _) -> [] ([h | t], f) -> [f.(h) | dumbmap(t, f)] end
is expanded to
@spec dumbmap([a], (a -> b)) :: [b] when a: term, b: term def dumbmap(as, f) def dumbmap([], _) do [] end def dumbmap([h | t], f) do [f.(h) | dumbmap(t, f)] end
-
-
In addition to the shorthand syntax explained above,
defun
is able to generate code for runtime type checking:- guard:
soma_arg :: g[integer]
- validation with
valid?/1
of a type module (see below):some_arg :: v[SomeType.t]
- guard:
-
There are also
defunp
anddefunpt
macros for private functions.
-
Sometimes you may want to have more fine-grained control of data types than is allowed by Elixir's typespec. For example you may want to distinguish "arbitrary
String.t
" with "String.t
that matches a specific regex". Croma introduces "type module"s in order to express fine-grained types and enforce type contracts at runtime, with minimal effort. -
Leveraging Elixir's lightweight syntax for defining modules (i.e. you can easily make multiple modules within a single source file), croma encourages you to define lots of small modules to organize code, especially types, in your Elixir projects. Croma expects that a type is defined in its dedicated module, which we call a "type module". This way a type can have associated functions within its type module.
-
The following definitions in type modules are used by croma:
@type t
- The type represented in Elixir's typespec.
valid?(any) :: boolean
- Runtime check of whether a given value belongs to the type.
Used by validation of arguments and return values in
defun
-family of macros.
- Runtime check of whether a given value belongs to the type.
Used by validation of arguments and return values in
new(any) :: {:ok, t} | {:error, any}
- Tries to convert a given value to a value that belongs to this type. Useful e.g. when converting a JSON-parsed value into an Elixir value.
default() :: t
- Default value of the type. Used as default values of struct fields.
@type t
andvalid?/1
are mandatory as they are the raison d'etre of a type module, but the others can be omitted. And of course you can define any other functions in your type modules as you like. -
You can always define your type modules by directly implementing above functions. For simple type modules croma prepares some helpers for you:
- type modules of built-in types such as
Croma.String
,Croma.Integer
, etc. - helper modules such as
Croma.SubtypeOfString
to define "subtype"s of existing types Croma.Struct
for structs- ad-hoc module generator macros defined in
Croma.TypeGen
- type modules of built-in types such as
-
You can define your type module for "
String.t
that matches~r/foo|bar/
" as follows (we usedefun
here but you can of course use@spec
anddef
instead):defmodule MyString1 do @type t :: String.t defun valid?(t :: term) :: boolean do s when is_binary(s) -> s =~ ~r/foo|bar/ _ -> false end end
-
However, as this is a common pattern, croma provides a shortcut:
defmodule MyString2 do use Croma.SubtypeOfString, pattern: ~r/foo|bar/ end
-
There are also
SubtypeOfInt
,SubtypeOfFloat
and so on.
-
Defining a type module for a struct can be tedious since you have to check all fields in the struct.
-
Using type modules for struct fields,
Croma.Struct
generates definition of type module for a struct.defmodule I do use Croma.SubtypeOfInt, min: 1, max: 5, default: 1 end defmodule S do use Croma.Struct, fields: [ i: I, f: Croma.Float, ] end S.valid?(%S{i: 5, f: 1.5}) # => true S.valid?(%S{i: "not_int", f: 1.5}) # => false {:ok, s} = S.new(%{f: 1.5}) # => {:ok, %S{i: 1, f: 1.5}} # `update/2` is also generated for convenience S.update(s, [i: 5]) # => {:ok, %S{i: 5, f: 1.5}} S.update(s, %{i: 6}) # => {:error, {:invalid_value, [S, I]}}
-
Suppose you have a type module
I
, and suppose you want to define a struct that have a field with typenil | I.t
. As nilable fields are common, defining type modules for all nilable fields introduces too much boilerplate code. -
Croma has a set of macros to define this kind of trivial type modules in-line. For example you can write as follows using
nilable/1
:defmodule S do use Croma.Struct, fields: [ i: Croma.TypeGen.nilable(I), ] end
- In
0.7.0
we separated responsibility ofvalidate/1
intovalid?/1
andnew/1
.- Although older type module implementations that define
validate/1
should work as before, please migrate to the newer interface by replacingvalidate/1
withvalid?/1
and optionallynew/1
. - In
0.8.0
we removed support ofvalidate/1
.
- Although older type module implementations that define