Elixir macro utilities to make type-based programming easier
Elixir
131
334 commits
updated Jul 9, 2026
Elixir macro utilities to make type-based programming easier.
:croma as a mix dependency.$ mix deps.get.use Croma in your source file to import/require macros defined in croma.Croma.ResultCroma.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 as get/2, get!/1, map/2, map_error/2, bind/2 and sequence/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 by Croma.Monad.)
Croma.Defun : Typespec-oriented function definitionAnnotating 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 and def.
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:
soma_arg :: g[integer]valid?/1 of a type module (see below): some_arg :: v[SomeType.t]There are also defunp and defunpt 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
valid?(any) :: boolean
defun-family of macros.new(any) :: {:ok, t} | {:error, any}
default() :: t
@type t and valid?/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:
Croma.String, Croma.Integer, etc.Croma.SubtypeOfString to define "subtype"s of existing typesCroma.Struct for structsCroma.TypeGenCroma.SubtypeOf*You can define your type module for "String.t that matches ~r/foo|bar/" as follows
(we use defun here but you can of course use @spec and def 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.
Croma.StructDefining 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]}}
Croma.TypeGenSuppose you have a type module I, and suppose you want to define a struct that have a field with type nil | 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
0.7.0 we separated responsibility of validate/1 into valid?/1 and new/1.
validate/1 should work as before,
please migrate to the newer interface by replacing validate/1 with valid?/1 and optionally new/1.0.8.0 we removed support of validate/1.Elixir
100.0%
Elixir macro utilities to make type-based programming easier
Elixir
131
334 commits
updated Jul 9, 2026
Elixir macro utilities to make type-based programming easier.
:croma as a mix dependency.$ mix deps.get.use Croma in your source file to import/require macros defined in croma.Croma.ResultCroma.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 as get/2, get!/1, map/2, map_error/2, bind/2 and sequence/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 by Croma.Monad.)
Croma.Defun : Typespec-oriented function definitionAnnotating 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 and def.
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:
soma_arg :: g[integer]valid?/1 of a type module (see below): some_arg :: v[SomeType.t]There are also defunp and defunpt 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
valid?(any) :: boolean
defun-family of macros.new(any) :: {:ok, t} | {:error, any}
default() :: t
@type t and valid?/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:
Croma.String, Croma.Integer, etc.Croma.SubtypeOfString to define "subtype"s of existing typesCroma.Struct for structsCroma.TypeGenCroma.SubtypeOf*You can define your type module for "String.t that matches ~r/foo|bar/" as follows
(we use defun here but you can of course use @spec and def 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.
Croma.StructDefining 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]}}
Croma.TypeGenSuppose you have a type module I, and suppose you want to define a struct that have a field with type nil | 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
0.7.0 we separated responsibility of validate/1 into valid?/1 and new/1.
validate/1 should work as before,
please migrate to the newer interface by replacing validate/1 with valid?/1 and optionally new/1.0.8.0 we removed support of validate/1.Elixir
100.0%