Current section

Files

Jump to
caffeine lib caffeine.ex
Raw

lib/caffeine.ex

defmodule Caffeine do
@moduledoc """
![Coffee Bean Boundary](./coffee.jpeg)
An alternative stream library
Find the API under `Caffeine.Stream`.
"""
defmodule Element do
@type t :: term
end
defmodule Stream do
@moduledoc """
Primitives and HOFs
## Examples
iex> defmodule Fibonacci do
...> import Caffeine.Stream, only: [construct: 2]
...>
...> def stream do
...> stream(0, 1)
...> end
...>
...> defp stream(a, b) do
...> rest = fn -> stream(b, a + b) end
...> construct(a, rest)
...> end
...> end
iex> require Integer
iex> Fibonacci.stream()
...> |> Caffeine.Stream.filter(&Integer.is_even/1)
...> |> Caffeine.Stream.map(&Integer.to_string/1)
...> |> Caffeine.Stream.take(10)
["0", "2", "8", "34", "144", "610", "2584", "10946", "46368", "196418"]
"""
@typedoc """
The Caffeine.Stream data structure
"""
@opaque t :: nonempty_improper_list(Element.t(), (() -> t)) | []
@doc """
This signals the end of a stream
Don't count on `sentinel/0` returning a `[]`.
SoE: how can we do this with a Dialyzer declaration?
"""
@spec sentinel() :: []
def sentinel do
[]
end
@doc """
Predicate: is _s_ the sentinel?
## Examples
iex> import Caffeine.Stream, only: [sentinel?: 1, sentinel: 0, construct: 2]
Caffeine.Stream
iex> sentinel?(sentinel())
true
iex> sentinel?(construct("Elixir", fn -> sentinel() end))
false
"""
@spec sentinel?(t) :: boolean
def sentinel?(s)
def sentinel?([]) do
true
end
def sentinel?([_ | x]) when is_function(x, 0) do
false
end
@doc """
Predicate: is _s_ a stream of at least one element?
## Examples
iex> import Caffeine.Stream, only: [construct?: 1, sentinel: 0, construct: 2]
Caffeine.Stream
iex> construct?(construct("Elixir", fn -> sentinel() end))
true
iex> construct?(sentinel())
false
"""
@spec construct?(t) :: boolean
def construct?(s)
def construct?([_ | x]) when is_function(x, 0) do
true
end
def construct?([]) do
false
end
@doc """
A stream of at least one element _h_
"""
@spec construct(Element.t(), (() -> t)) :: t
def construct(h, t) when is_function(t, 0) do
pair(h, t)
end
@doc """
A list of _n_ consecutive elements from the stream _s_
## Examples
iex> defmodule Natural do
...> import Caffeine.Stream, only: [construct: 2]
...>
...> def stream do
...> stream(0)
...> end
...>
...> defp stream(n) do
...> rest = fn -> stream(increment(n)) end
...> construct(n, rest)
...> end
...>
...> defp increment(n) do
...> n + 1
...> end
...> end
iex> Caffeine.Stream.take(Natural.stream(), 5)
[0,1,2,3,4]
"""
@spec take(t, integer) :: list
def take([], _) do
[]
end
def take(_, 0) do
[]
end
def take(s, n) when is_integer(n) and n > 0 do
[head(s) | take(tail(s), n - 1)]
end
@doc """
Like the stream _s_ with the function _f_ applied to each element
## Examples
iex> defmodule Natural do
...> import Caffeine.Stream, only: [construct: 2]
...>
...> def stream do
...> stream(0)
...> end
...>
...> defp stream(n) do
...> rest = fn -> stream(increment(n)) end
...> construct(n, rest)
...> end
...>
...> defp increment(n) do
...> n + 1
...> end
...> end
iex> Natural.stream()
...> |> Caffeine.Stream.map(&Integer.to_string/1)
...> |> Caffeine.Stream.take(5)
["0","1","2","3","4"]
"""
@spec map(t, (Element.t() -> Element.t())) :: t
def map(s, f) do
# import Caffeine.Stream, only: [sentinel?: 1, construct?: 1, sentinel: 0, construct: 2]
cond do
sentinel?(s) ->
sentinel()
construct?(s) ->
g = fn -> map(tail(s), f) end
construct(f.(head(s)), g)
end
end
@doc """
A stream whose elements prescribe to the predicate _p_
## Examples
iex> defmodule Natural do
...> import Caffeine.Stream, only: [construct: 2]
...>
...> def stream do
...> stream(0)
...> end
...>
...> defp stream(n) do
...> rest = fn -> stream(increment(n)) end
...> construct(n, rest)
...> end
...>
...> defp increment(n) do
...> n + 1
...> end
...> end
iex> require Integer
iex> Natural.stream()
...> |> Caffeine.Stream.filter(&Integer.is_even/1)
...> |> Caffeine.Stream.take(5)
[0,2,4,6,8]
"""
@spec filter(t, (Element.t() -> boolean)) :: t
def filter(s, p) do
# import Caffeine.Stream, only: [sentinel?: 1, construct?: 1, sentinel: 0, construct: 2]
cond do
sentinel?(s) ->
sentinel()
construct?(s) ->
_filter(s, p)
end
end
@doc """
The head, if any, of the stream _s_
The following always holds true `head(construct(X, Y)) == X`.
## Examples
iex> import Caffeine.Stream, only: [head: 1, sentinel: 0, construct: 2]
Caffeine.Stream
iex> h = "Elixir"
iex> t = fn -> sentinel() end
iex> s = construct(h, t)
iex> head(s) == h
true
"""
@spec head(t) :: Element.t()
def head(s)
def head([h | t]) when is_function(t, 0) do
h
end
@doc """
The tail, if any, of the stream _s_
The following always holds true `tail(construct(X, Y)) == Y.()`.
## Examples
iex> import Caffeine.Stream, only: [tail: 1, sentinel: 0, construct: 2]
Caffeine.Stream
iex> h = "Elixir"
iex> t = fn -> sentinel() end
iex> s = construct(h, t)
iex> tail(s) == sentinel()
true
"""
@spec tail(t) :: t
def tail(s)
def tail([_ | t]) when is_function(t, 0) do
t.()
end
defp pair(h, r) do
[h | r]
end
defp _filter(s, p) do
if p.(head(s)) do
g = fn -> filter(tail(s), p) end
construct(head(s), g)
else
filter(tail(s), p)
end
end
end
end