View Source Witchcraft.Chain (Witchcraft v1.0.6-doma)

Chain function applications on contained data that may have some additional effect

As a diagram:

%Container<data> --- (data -> %Container<updated_data>) ---> %Container<updated_data>

examples

Examples

iex> chain([1, 2, 3], fn x -> [x, x] end)
[1, 1, 2, 2, 3, 3]

alias Algae.Maybe.{Nothing, Just}

%Just{just: 42} >>> fn x -> %Just{just: x + 1} end
#=> %Just{just: 43}

%Just{just: 42}
>>> fn x -> if x > 50, do: %Just{just: x + 1}, else: %Nothing{} end
>>> fn y -> y * 100 end
#=> %Nothing{}

type-class

Type Class

An instance of Witchcraft.Chain must also implement Witchcraft.Apply, and define Witchcraft.Chain.chain/2.

Functor  [map/2]
   
 Apply   [convey/2]
   
 Chain   [chain/2]

Link to this section Summary

Functions

Operator alias for draw/2

Operator alias for chain/2.

do notation sugar

Sequentially compose actions, piping values through successive function chains.

Compose link functions to create a new link function.

chain/2 but with the arguments flipped.

Join together one nested level of a data structure that contains itself

Compose link functions to create a new link function.

Link to this section Types

@type link() :: (any() -> t())
@type t() :: any()

Link to this section Functions

@spec t() <<< link() :: t()

Operator alias for draw/2

Extends the <~ / <<~ heirarchy with one more level of power / abstraction

examples

Examples

iex> to_monad = fn x -> (fn _ -> x end) end
...> bound = to_monad.(&(&1 + 10)) <<< to_monad.(&(&1 * 10))
...> bound.(10)
20

In Haskell, this is the famous =<< operator, but Elixir doesn't allow that infix operator.

@spec t() >>> link() :: t()

Operator alias for chain/2.

Extends the ~> / ~>> heirarchy with one more level of power / abstraction

examples

Examples

iex> to_monad = fn x -> (fn _ -> x end) end
...> bound = to_monad.(&(&1 * 10)) >>> to_monad.(&(&1 + 10))
...> bound.(10)
20

In Haskell, this is the famous >>= operator, but Elixir doesn't allow that infix operator.

@spec bind(t(), link()) :: t()

An alias for chain/2.

Provided as a convenience for those coming from other languages.

do notation sugar

Sequences chainable actions. Note that each line must be of the same type.

For a version with return, please see Witchcraft.Monad.monad/2

examples

Examples

iex> chain do
...>   [1]
...> end
[1]

iex> chain do
...>   [1, 2, 3]
...>   [4, 5, 6]
...>   [7, 8, 9]
...> end
[
  7, 8, 9,
  7, 8, 9,
  7, 8, 9,
  7, 8, 9,
  7, 8, 9,
  7, 8, 9,
  7, 8, 9,
  7, 8, 9,
  7, 8, 9
]

iex> chain do
...>   a <- [1, 2, 3]
...>   b <- [4, 5, 6]
...>   [a * b]
...> end
[
  4,  5,  6,
  8,  10, 12,
  12, 15, 18
]

Normal functions are fine within the do as well, as long as each line ends up being the same chainable type

iex> import Witchcraft.{Functor, Applicative}
...> chain do
...>   map([1, 2, 3], fn x -> x + 1 end)
...>   of([], 42)
...>   [7, 8, 9] ~> fn x -> x * 10 end
...> end
[
  70, 80, 90,
  70, 80, 90,
  70, 80, 90
]

Or with a custom type

alias Algae.Maybe.{Nothing, Just}

chain do
  %Just{just: 4}
  %Just{just: 5}
  %Just{just: 6}
end
#=> %Just{just: 6}

chain do
  %Just{just: 4}
  %Nothing{}
  %Just{just: 6}
end
#=> %Nothing{}

let-bindings

let bindings

lets allow you to hold static or intermediate values inside a do-block, much like normal assignment

iex> chain do
...>   let a = 4
...>   [a]
...> end
[4]

iex> chain do
...>   a <- [1, 2]
...>   b <- [3, 4]
...>   let [h | _] = [a * b]
...>   [h, h, h]
...> end
[3, 3, 3, 4, 4, 4, 6, 6, 6, 8, 8, 8]

desugaring

Desugaring

sequencing

Sequencing

The most basic form

chain do
  [1, 2, 3]
  [4, 5, 6]
  [7, 8, 9]
end

is equivalent to

[1, 2, 3]
|> then([4, 5, 6])
|> then([7, 8, 9])

drawn-from

<- ("drawn from")

Drawing values from within a chainable structure is similar feels similar to assignmet, but it is pulling each value separately in a chain link function.

For instance

iex> chain do
...>   a <- [1, 2, 3]
...>   b <- [4, 5, 6]
...>   [a * b]
...> end
[4, 5, 6, 8, 10, 12, 12, 15, 18]

desugars to this

iex> [1, 2, 3] >>> fn a ->
...>   [4, 5, 6] >>> fn b ->
...>     [a * b]
...>   end
...> end
[4, 5, 6, 8, 10, 12, 12, 15, 18]

but is often much cleaner to read in do-notation, as it cleans up all of the nested functions (especially when the chain is very long).

You can also use values recursively:

# iex> chain do
# ...>   a <- [1, 2, 3]
# ...>   b <- [a, a * 10, a * 100]
# ...>   [a + 1, b + 1]
# ...> end
# [
#   2, 2, 2, 11, 2, 101,
#   3, 3, 3, 21, 3, 201,
#   4, 4, 4, 31, 4, 301
# ]
Link to this function

chain(chainable, link_fun)

View Source
@spec chain(t(), link()) :: t()

Sequentially compose actions, piping values through successive function chains.

The applied linking function must be unary and return data in the same type of container as the input. The chain function essentially "unwraps" a contained value, applies a linking function that returns the initial (wrapped) type, and collects them into a flat(ter) structure.

chain/2 is sometimes called "flat map", since it can also be expressed as data |> map(link_fun) |> flatten().

As a diagram:

%Container<data> --- (data -> %Container<updated_data>) ---> %Container<updated_data>

examples

Examples

iex> chain([1, 2, 3], fn x -> [x, x] end)
[1, 1, 2, 2, 3, 3]

iex> [1, 2, 3]
...> |> chain(fn x -> [x, x] end)
...> |> chain(fn y -> [y, 2 * y, 3 * y] end)
[1, 2, 3, 1, 2, 3, 2, 4, 6, 2, 4, 6, 3, 6, 9, 3, 6, 9]

iex> chain([1, 2, 3], fn x ->
...>   chain([x + 1], fn y ->
...>     chain([y + 2, y + 10], fn z ->
...>       [x, y, z]
...>     end)
...>   end)
...> end)
[1, 2, 4, 1, 2, 12, 2, 3, 5, 2, 3, 13, 3, 4, 6, 3, 4, 14]
Link to this function

compose_link(action_g, action_f)

View Source
@spec compose_link(link(), link()) :: link()

Compose link functions to create a new link function.

This is pipe_compose_link/2 with arguments flipped.

examples

Examples

iex> links =
...>   fn x -> [x, x] end
...>   |> compose_link(fn y -> [y * 10] end)
...>   |> compose_link(fn z -> [z + 42] end)
...>
...> [1, 2, 3] >>> links
[430, 430, 440, 440, 450, 450]
Link to this function

draw(chain_fun, chainable)

View Source
@spec draw(link(), t()) :: t()

chain/2 but with the arguments flipped.

examples

Examples

iex> draw(fn x -> [x, x] end, [1, 2, 3])
[1, 1, 2, 2, 3, 3]

iex> (fn y -> [y * 5, y * 10] end)
...> |> draw((fn x -> [x, x] end)
...> |> draw([1, 2, 3])) # note the "extra" closing paren
[5, 10, 5, 10, 10, 20, 10, 20, 15, 30, 15, 30]
@spec flatten(t()) :: t()

See Witchcraft.Chain.join/1.

@spec join(t()) :: t()

Join together one nested level of a data structure that contains itself

examples

Examples

iex> join([[1, 2, 3]])
[1, 2, 3]

iex> join([[1, 2, 3], [4, 5, 6]])
[1, 2, 3, 4, 5, 6]

iex> join([[[1, 2, 3], [4, 5, 6]]])
[[1, 2, 3], [4, 5, 6]]

alias Algae.Maybe.{Nothing, Just}
%Just{
  just: %Just{
    just: 42
  }
} |> join()
#=> %Just{just: 42}

join %Just{just: %Nothing{}}
#=> %Nothing{}

join %Just{just: %Just{just: %Nothing{}}}
#=> %Just{just: %Nothing{}}

%Nothing{} |> join() |> join() |> join() # ...and so on, forever
#=> %Nothing{}

Joining tuples is a bit counterintuitive, as it requires a very specific format:

iex> join {      # Outer 2-tuple
...>   {1, 2},   # Inner 2-tuple
...>   {
...>     {3, 4}, # Doubly inner 2-tuple
...>     {5, 6, 7}
...>   }
...> }
{{4, 6}, {5, 6, 7}}

iex> join {
...>   {"a", "b"},
...>   {
...>     {"!", "?"},
...>     {:ok, 123}
...>   }
...> }
{{"a!", "b?"}, {:ok, 123}}
Link to this function

pipe_compose_link(action_f, action_g)

View Source
@spec pipe_compose_link(link(), link()) :: link()

Compose link functions to create a new link function.

This is compose_link/2 with arguments flipped.

examples

Examples

iex> links =
...>   fn x -> [x, x] end
...>   |> pipe_compose_link(fn y -> [y * 10] end)
...>   |> pipe_compose_link(fn z -> [z + 42] end)
...>
...> [1, 2, 3] >>> links
[52, 52, 62, 62, 72, 72]