Elixirの型クラス周りのライブラリである Algae について紹介します
あくまで使い方について紹介するだけでモナドについての難しい話はしません
Algae
代数的データ型とそれに対する型クラスインスタンスの定義を含むライブラリです
Elixirの世界にMaybeやEitherをもたらしてくれるのでカジュアルに使っても
nilセーフになったり、パターンマッチヘルなどを回避できるなど恩恵があります
今回では定義されている代数的データや、defmacroで定義されているdo構文をみていきましょう
依存性
Algaeは前項で述べたとおり代数的データ型とそれに対する型クラスインスタンスの定義のライブラリです
型クラス機能自体の実装は type_class
各型クラス(Functor, Applicative, Monad. etc...)の実装は Witchcraft
にあります
do構文
do構文はWitchcraftの lib/witchcraft/chain.ex
に定義されています
Haskellと違い Chain
型クラスを実装していれば使用することができます
(ただし、returnの使い方が後述のmonadマクロを使用する上、少し特殊です)
import Witchcraft.Chain
alias Algae.Maybe
alias Algae.Maybe.{Just, Nothing}
def f(nillable, n) do
chain do
a <- Maybe.from_nillable(nillable)
b <- safe_div(a, n)
Maybe.new(a + b)
end
end
def safe_div(a, b) do
if b != 0, do: %Just{just: a / b}, else: %Nothing{}
end
iex(31)> f(1, 3)
%Algae.Maybe.Just{just: 1.3333333333333333}
iex(32)> f(nil, 3)
%Algae.Maybe.Nothing{}
iex(33)> f(1, 0)
%Algae.Maybe.Nothing{}
iex(34)>
また、lib/witchcraft/monad.ex
に定義されているmonad macroを使うことでreturnを使用することも可能になります
def f(nillable, n) do
monad Maybe.from_nillable(nil) do
a <- Maybe.from_nillable(nillable)
b <- safe_div(a, n)
return (a + b)
end
end
monad macro第一引数として渡している Maybe.from_nillable
は return
を呼び出したときに実際に呼び出される関数となります、from_nillable
に渡している引数値は無視されるのでなんでもいいのですが、なんらかの値を渡さない場合は Maybe.from_nillable/0
の関数として認識されてしまうので必須です
returnにnil値を渡さないとわかっているのであれば Maybe.new()
でも良いでしょう
do構文を使わずbind関数で書く場合はこうなります
(Haskellのbind (>>=
) とは違い、bindのaliasは >>>
になっています)
def f(nillable, n) do
Maybe.from_nillable(nillable)
>>> fn a -> safe_div(a, n)
>>> fn b -> Maybe.new(a + b) end end
end
…見ての通りネストするほど end
が多くなっていくのでdo構文で書くことをオススメします
ちなみに let
による変数束縛もちゃんと使えます
def f(nillable, n) do
chain do
a <- Maybe.from_nillable(nillable)
b <- safe_div(a, n)
let c = 10
Maybe.new(a + b + c)
end
end
Maybe
- Haskell
data Maybe a = Just a
| Nothing
- Elixir(Algae)
defsum do
defdata Nothing :: none()
defdata Just :: any()
end
defsum
は直和 defdata
は直積を定義するためのマクロです
Elixirには型パラメタの機能はないので Maybe a
にあたる a
は any
になります
-
Maybe.new/0
Maybe.new/1
/Maybe.from_nillable/1
コンストラクタとしては二種類用意されています
Maybe.new(nil)
とした場合は%Just{just: nil}
になってしまうので基本的にはMaybe.from_nillable
を使うことを推奨します -
Maybe.from_maybe/2
第一引数にMaybe
型を受け取りJustの場合はその中身を、Nothingの場合は第二引数の値を返します
Either
- Haskell
data Either a b = Left a
| Right b
defsum do
defdata Left :: any()
defdata Right :: any()
end
Elixirには型パラメタの機能はない(ry
-
Right.new/1
/Left.new/1
Eitherにおけるコンストラクタです -
example
def fE(nillable, n) do
chain do
a <- case nillable do
nil -> Left.new("nil is can't division.")
n -> Right.new(n)
end
b <- safe_divE(a, n)
Right.new(a + b)
end
end
def safe_divE(a, b) do
if b != 0, do: Right.new(a / b), else: Left.new("n is 0.")
end
iex(50)> fE(1, 3)
%Algae.Either.Right{right: 1.3333333333333333}
iex(51)> fE(nil, 3)
%Algae.Either.Left{left: "nil is can't division."}
iex(52)> fE(1, 0)
%Algae.Either.Left{left: "n is 0."}
State
newtype State s a = State { runState :: s -> (a,s) }
@type runner :: (any() -> {any(), any()})
@type t :: %State{runner: runner()}
Maybe/Eitherと比較するとかなり特殊ですね
Stateの実態は、状態値 s
を受け取って結果値 a
と新しい状態 s
を返す関数なのでこのような特殊な形になります
HaskellではStateを渡すときに runState
で渡しますがAlgaeでは runner
に(実態の関数に)渡します
Algae.State.state(fn x -> {x + 1, x} end).runner.(42)
-
State.new/1
/State.state/1
any() -> {any(), any()}
の関数を受け取ってState型を返す
つまりコンストラクタです
state関数はnew関数のエイリアスです -
put/1
/get/0
Stateモナドコンテキスト内で使える値のセットとゲットです
と書くよりdo構文で使っているのを見るほうが理解できるでしょう
def fS() do
init = State.state(fn s -> {0, s} end)
chain do
i <- init
n <- State.get()
State.put(99) ~> (fn _z -> i + n end)
end
end
iex(65)> A.fS().runner.(3)
{3, 99}
( ~>
はfmap( <$>
)です)
その他
状態を変更する関数を受け取る modify
Stateモナドから結果値のみを取得する evaluate
Stateモナドから状態のみを取得する execute
(使い道あるのかな?)
などがありますが割愛します
今回紹介した他 writer
reader
free
などおなじみのモナドたちが定義されていますが
そちらの紹介はまた次の機会という形にします
Haskellのように強い型付けがないのでElixirの世界ではモナドを利用したとしても
完全に副作用の切り離しを保証できるものではありません
ですがdo構文などコードを完結に書く力は得られるので使う価値のないものでもありません
Elixirの世界にモナドを取り入れられるこのライブラリを僕は素晴らしいものだと思います