View Source Bindable (Bindable v1.1.0)

Elixir for-comprehension that goes beyond the Lists.

  iex> import Bindable.ForComprehension
  ...> import Bindable.Maybe
  ...>
  ...> bindable for x <- just(1),
  ...>              y <- just(2),
  ...>              x + y > 4,
  ...>              z <- just(3),
  ...>          do: x + y + z
  nothing()

For-comprehension is a common syntax construct in functional programming languages, that allows you to express complex operations on collections and monadic contexts in a concise and expressive way. More formally, it is a way to construct monadic value by describing it as a sequence of effectful computations. To do so, it provides following constructs to combine such computations into new monadic value:

  • Generator (commonly known as iterator for lists): extracts value from the monadic context.
  • Guard (or filter): filters generated values based on a predicate (see Bindable.Empty).
  • Assign: aliases any expression inside current scope.
  • Yield: defines resulting value to return inside the monadic context.

Here comes an example of them:

  iex> import Bindable.ForComprehension
  ...>
  ...> xs = [[10, 20], [30]]
  ...> bindable for x <- xs,       # generator
  ...>              length(x) > 1, # guard
  ...>              y <- x,        # next generator
  ...>              z = y + 1,     # assign
  ...>              y + z > 21,    # another guard
  ...>          do: {y, z}         # yield
  [{20, 21}]

Elixir's kernel provides for-comprehension only for lists. It works with any Enumerable, however it always (eagerly) yields List (so you can't describe Stream using Kernel.SpecialForms.for/1). Bindable already comes with for-comprehension batteries for:

So you can lazily create new stream out of provided ones:

  iex> import Bindable.ForComprehension
  ...>
  ...> lazy_xs = Stream.take(1..5, 2)
  ...> lazy_ys = Stream.take(5..9, 2)
  ...> lazy_xys = bindable for x <- lazy_xs,
  ...>                         y <- lazy_ys,
  ...>                     do: {x, y}
  ...>
  ...> {is_list(lazy_xys), Enum.to_list(lazy_xys)}
  {false, [{1, 5}, {1, 6}, {2, 5}, {2, 6}]}

The main goal of the library is to provide for-comprehension beyond lists with the least amount of overhead. So it doesn't aim to provide a principled way to define type classes (required by for-context), e.g. it doesn't provide any sensible way to enforce type class properties on implementations. To stick with "the least amount of overhead" paradigm type classes implemented atop of Elixir protocols.

"Minimal complete definition" for you data type to be compliant with for-comprehension includes:

If you want to use guards/filters inside for-comprehension with your data type (or you want to pattern match on the generated values like Kernel.SpecialForms.for/1 does, as in this case pattern matching has filtering semantics), you should also provide an implementation for Bindable.Empty, so it is optional, e.g. when your data type does not provide any meaningful semantics for empty/filtered value effect:

  iex> import Bindable.ForComprehension
  ...>
  ...> xs =  [[], [2, 2], [3], [4]]
  ...> xys = [[1, 1], [2, 2], [3, 3], [4, 4]]
  ...> bindable for [x] <- xs,      # pattern matching guard
  ...>              [^x, y] <- xys, # pattern matching guard with ^ operator
  ...>              y > 0,          # "conventional" guard
  ...>              do: [x, y]
  [[3, 3], [4, 4]]