defmodule Containers do @moduledoc """ Containers are functional data structures that help provide greater runtime safety and polymorphism. ## Protocols * `Appendable` - A container that provies an interface of `append`. Safe against `nil` values. Namely when passing a container with the value `nil` into either the first of second argument to `append`, the other value is not change and there is no runtime error. * `Mappable` - A container that provies an interface to `map`. When `map` is called on a container that has a `nil` value that container just passes through with out the mapping function being called, and this helps prevent runtime errors. * `Sequenceable` - A container that provides an interface of `next`. This allows the chaining of computations. * `Unwrappable` - A container that provides an interface to `safe` and `unsafe` unwrapping of inner value. Safe will need a default in case of `nil` value of container, helping prevent runtime errors. Unsafe will just return the value of the container regardless of a `nil` value potentially causing runtime errors Since these are protocols, and highly decoupled, a developer can implement them as needed on their own structs. """ @type appendable :: Containers.Text.t | Containers.Optional.t @doc """ Append two values of the Containers.Appendable protocol This is useful for chaning of appending appendable items safely. That is to say if there is a `nil` value being used like `nil <> " world!"` there will be a run time error. In this case the container for the string type will safe do concatenation. ## Examples iex> hello = Containers.Text.from_string("Hello") iex> world = Containers.Text.from_string(" world!") iex> Containers.append(hello, world) %Containers.Text{value: "Hello world!"} iex> hello = Containers.Text.from_string("Hello") iex> world = Containers.Text.from_string(" world!") iex> nil_string = Containers.Text.from_string(nil) iex> hello |> Containers.append(nil_string) |> Containers.append(world) %Containers.Text{value: "Hello world!"} """ @spec append(struct(), struct()) :: struct() def append(v1, v2), do: Containers.Appendable.append(v1, v2) @doc """ map some function `f` of the some structure `s`. Works like the `Enum.map` but provides more polymorphic protocol, and does rely on the `Enumerable` protocol allowing use of just getting map without needing to implement the full `Enumerable` protocol. ## Examples iex> my_optional = Containers.Optional.to_optional(1) iex> Containers.map(my_optional, fn(i) -> i + 1 end) %Containers.Optional{value: 2} """ @spec map(struct(), (... -> any())) :: struct() def map(s, f), do: Containers.Mappable.map(s, f) @doc """ next is a function that will allow chaining of computations while passing the `value` of the last computation. """ @spec next(struct(), (... -> struct())) :: struct() def next(s, f), do: Containers.Sequenceable.next(s, f) @doc """ `>>>` is the infix operator for `next` ## Examples iex> import Containers iex> my_optional = Containers.Optional.to_optional(1) iex> my_optional >>> fn(i) -> Containers.Optional.to_optional(i + 1) end %Containers.Optional{value: 2} """ def s >>> f, do: Containers.Sequenceable.next(s, f) @doc """ safely unwrap the inner value of a container, proviing a default in case the value is `nil`. This is should help prevent runtime errors within a `|>` chain handling strings. ## Examples iex> my_string = Containers.Text.from_string("hello") iex> Containers.safe_unwrap(my_string, "this wont be needed") "hello" iex> my_nil_string = Containers.Text.from_string(nil) iex> Containers.safe_unwrap(my_nil_string, "This will be the value") "This will be the value" """ @spec safe_unwrap(struct(), any()) :: any() def safe_unwrap(s, default), do: Containers.Unwrappable.safe(s, default) @doc """ unsafely unwrap the inner value of a continer. This may return nil so any guarantees against a runtime error no longer apply. ## Examples iex> my_string = Containers.Text.from_string("Hello") iex> Containers.unsafe_unwrap(my_string) "Hello" iex> my_nil_string = Containers.Text.from_string(nil) iex> Containers.unsafe_unwrap(my_nil_string) nil """ @spec unsafe_unwrap(struct()) :: any() def unsafe_unwrap(s), do: Containers.Unwrappable.unsafe!(s) @doc """ concat a list of Containers that implement the Appendable protocol ``` hello_world = "hello_world" hello_world |> String.split("_") |> Enum.map(&String.capitalize/1) |> Enum.map(&Containers.Text.from_string/1) |> Containers.concat() |> Containers.safe_unwrap("") "HelloWorld" ``` ## Examples iex> hello = Containers.Text.from_string("hello") iex> world = Containers.Text.from_string(" world") iex> excliam = Containers.Text.from_string("!") iex> Containers.concat([hello, world, excliam]) %Containers.Text{value: "hello world!"} """ @spec concat(list()) :: appendable() def concat(appendables) do appendables |> Enum.reverse |> Enum.reduce(&append/2) end end