Croma
Elixir macro utilities to make type-based programming easier.
Usage
-
Add
:cromaas a mix dependency. -
Run
$ mix deps.get. -
Add
use Cromain your source file to import/require macros defined in croma. - Hack!
Croma.Result
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/2andsequence/1.You can also use Haskell-like do-notation to combine results of multiple computations by
m/1macro. For example,Croma.Result.m do x <- {:ok, 1} y <- {:ok, 2} pure x + y endis 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.)
Croma.Defun : Typespec-oriented function definition
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
@specanddef.defun/2macro 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}" endis expanded to
@spec f(integer, String.t) :: String.t def f(a, b) do "#{a} #{b}" endExample 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)] endis 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,
defunis able to generate code for runtime type checking:-
guard:
soma_arg :: g[integer] -
validation with
valid?/1of a type module (see below):some_arg :: v[SomeType.t]
-
guard:
There are also
defunpanddefunptmacros for private functions.
Type modules
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.tthat 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. Must be a constant value. Used as default values of struct fields.
@type tandvalid?/1are 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.SubtypeOfStringto define “subtype”s of existing types Croma.Structfor structs-
ad-hoc module generator macros defined in
Croma.TypeGen
-
type modules of built-in types such as
Croma.SubtypeOf*
You can define your type module for “
String.tthat matches~r/foo|bar/“ as follows (we usedefunhere but you can of course use@specanddefinstead):defmodule MyString1 do @type t :: String.t defun valid?(t :: term) :: boolean do s when is_binary(s) -> s =~ ~r/foo|bar/ _ -> false end endHowever, as this is a common pattern, croma provides a shortcut:
defmodule MyString2 do use Croma.SubtypeOfString, pattern: ~r/foo|bar/ endThere are also
SubtypeOfInt,SubtypeOfFloatand so on.
Croma.Struct
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.Structgenerates 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]}}
Croma.TypeGen
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
Notes on backward compatibility
-
In
0.7.0we separated responsibility ofvalidate/1intovalid?/1andnew/1.-
Although older type module implementations that define
validate/1should work as before, please migrate to the newer interface by replacingvalidate/1withvalid?/1and optionallynew/1. -
In
0.8.0we removed support ofvalidate/1.
-
Although older type module implementations that define