Croma
=====
Elixir macro utilities to make type-based programming easier.
- [API Documentation](http://hexdocs.pm/croma/)
- [Hex package information](https://hex.pm/packages/croma)
[![Hex.pm](http://img.shields.io/hexpm/v/croma.svg)](https://hex.pm/packages/croma)
[![Hex.pm](http://img.shields.io/hexpm/dt/croma.svg)](https://hex.pm/packages/croma)
[![Coverage Status](https://coveralls.io/repos/github/skirino/croma/badge.svg?branch=master)](https://coveralls.io/github/skirino/croma?branch=master)
## Usage
- 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`
- `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 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,
```ex
Croma.Result.m do
x <- {:ok, 1}
y <- {:ok, 2}
pure x + y
end
```
is converted to
```ex
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 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 `@spec` and `def`.
- `defun/2` macro provides shorthand syntax for defining function with its typespec at once.
- Example 1
```ex
use Croma
defun f(a :: integer, b :: String.t) :: String.t do
"#{a} #{b}"
end
```
is expanded to
```ex
@spec f(integer, String.t) :: String.t
def f(a, b) do
"#{a} #{b}"
end
```
- Example 2 (multi-clause syntax)
```ex
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
```ex
@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]`
- There are also `defunp` and `defunpt` macros 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](https://hexdocs.pm/elixir/typespecs.html).
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.
- `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 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:
- 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`
### `Croma.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):
```ex
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:
```ex
defmodule MyString2 do
use Croma.SubtypeOfString, pattern: ~r/foo|bar/
end
```
- There are also `SubtypeOfInt`, `SubtypeOfFloat` and 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.Struct` generates definition of type module for a struct.
```ex
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 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`:
```ex
defmodule S do
use Croma.Struct, fields: [
i: Croma.TypeGen.nilable(I),
]
end
```
## Notes on backward compatibility
- In `0.7.0` we separated responsibility of `validate/1` into `valid?/1` and `new/1`.
- Although older type module implementations that define `validate/1` should work as before,
please migrate to the newer interface by replacing `validate/1` with `valid?/1` and optionally `new/1`.
- In `0.8.0` we removed support of `validate/1`.