やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

数学全然わからないけどElixirでEitherを作ってみる

概要📖

  • Haskellではエラーが発生した際に対応するためにEitherというモナドを使う
  • EitherLeft(エラー時)Right(正常時)の2つの値を持ち、それぞれ別の型の値を受け取ることが可能(Either = Left a | Right b)
  • LeftにはStringがよく用いられ、エラーメッセージを格納することが多い
  • Eitherが必要なのは呼び出し元に、なぜエラーになったのかを伝えるため
  • Maybeではエラー、値なしの理由を呼び出しもとに伝えることが出来ない
  • Elixirで実装するにはTupleAtomを使うのが良さげ

前回までのあらすじ📖

「入門Haskellプログラミング」を読み進めながら、理解を深めるためにElixirを使ってHaskellFunctorApplicative, Monadを実装しました。

以上の3つで「入門Haskellプログラミング」のUNIT5で扱われていたコンテキストでの型の操作に関しての項目が終了しました。

いやー、長かった!抽象的な概念が続いたので理解するのにかなり苦労しましたが、何とかUNIT5を終了することが出来ました。

これでコンテキスト内に存在する値に対しての操作(関数適応)が可能となりました🎉

最後のモナド🔥

本書で紹介されているモナドはあと1つです。
Eitherと呼ばれるエラーを扱うためのモナドで、呼び出し元になぜエラーになったのかを伝えるために利用されています。 (The Either type is sometimes used to represent a value which is either correct or an errorより判断)

The Either type represents values with two possibilities: a value of type Either a b is either Left a or Right b.
The Either type is sometimes used to represent a value which is either correct or an error; by convention, the Left constructor is used to hold an error value and the Right constructor is used to hold a correct value (mnemonic: "right" also means "correct").

hackage.haskell.org

ドキュメントに書かれているように、EitherLeftRightという2つの値を持つことが出来ます。
通常、Leftはエラー(想定外)が発生した時に用いられ、Rightは正常な場合に使用します。

LeftRightはそれぞれ別の型の値を受け取ることが出来ます。本書をメインに使用例をいくつか確認しましたが、エラー時にはLeftString型のエラーメッセージを。正常時にはRightに結果の値を渡しているケースが多いようです。

data Either a b = Left a | Right b
    -- Defined in ‘Data.Either’

Left "例外が発生しました!"
Right True

なぜEitherが必要なのか🤔

Eitherの前身として紹介されていたのがMaybeというモナドです。Maybeは正常時にはJust aという値を返し、結果なしの場合にはNothingという値を返します。

maybeHead :: [a] -> Maybe a
maybeHead [] = Nothing
maybeHead (x:_) = Just x

Maybeは他言語でいうところの結果がnilnullになる場合に対応します。値が存在しない時にはNothingという結果をただ返します。

先ほどのheadという関数を空配列に対して呼び出すと、実は例外が発生します。

Prelude> head []
*** Exception: Prelude.head: empty list

Maybeとパターンマッチを使って結果を内包させることで、例外が発生するのを防ぐことが出来ます。
またMaybeMonadインスタンスであるため、今まで紹介してきたFunctor, Applicative, Monadなど多くの恩恵を受けることが出来ます。

しかしながら、ただ「Nothing」という値が返ってきただけでは、なぜ取得することが出来なかったのか呼び出し元で知る術がありません。

maybeHead([]) -- Nothing
-- 空配列には要素がないため本来は例外が発生する -> Maybeで対応 -> Nothingが返ってくるだけ

「空配列には要素がないからエラーだよ!」というメッセージが呼び出し元に返ってくると嬉しいケースがあります。(多くの人が使用するライブラリや関数を作成する場合など)

このような場合に対応するためにEitherが必要になります。

eitherHead :: [a] -> Either String a
eitherHead [] = Left "Emplty List!"
eitherHead (x:_) = Right x
eitherHead []
# Left "Emplty List!"

Elixirでの実装🧪

さっそくこのEitherElixirで実装してみます。(EitherElixirゲシュタルト崩壊する...)

まずですが、LeftRightという2つの値は構造体で定義するのは手間なので、Elixirの文化に従ってTuple({})を使用します。
Tupleを用いて:left:rightというAtomを使ってEitherを定義しました。

defmodule Either do
  @moduledoc """
    Implement Either in Elixir
    Left -> { :left, a(any) }
    Right -> { :right, b(any) }
  """
  def left(a), do: { :left, a } 
  def right(b), do: { :right, b } 
end

これでLeftRightという2つの値を表現することが出来ました。

Either.left("failed!")
# {:left, "failed!"}

Either.right("succcesed!")
# {:right, "succcesed!"} 

次にEitherを使ってhaed関数を定義します。空配列の場合にはLeftを返し、「空配列 -> エラー」ということが分かるメッセージを返します。
合わせて、リスト以外のデータが引数に指定された場合にも同じようにLeftを返します。

defmodule EitherExample do
  @moduledoc """
    haed with Either
    haed -> Fetch first value from List
  """
  def head([]), do: Either.left("Emplty List!")
  def head([h | _]), do: Either.right(h)
  def head(_), do: Either.left("head can only be used in List")
end

呼び出してみます。

EitherExample.head([1,2,3,4,5])
# {:right, 1}

EitherExample.head([])
# {:left, "Emplty List!"}

EitherExample.head("")
# {:left, "head can only be used in List"}

いい感じですね。いくつかテストを実行してみます。

  def debugger(input, result, except) do
    is_match = result == except
    "[#{status(is_match)}] #{inspect input} -> #{inspect result} == #{inspect except} is #{is_match}"
  end

  def status(false), do: "Failure"
  def status(true), do: "Success"

  def executer(input, except) do
    result = EitherExample.head(input)
    output_format = debugger(input, result, except)
    IO.puts(output_format)
  end
end

[
  { [1,2,3,4,5], Either.right(1) },
  { [], Either.left("Emplty List!") },
  { "", Either.left("Head can only be used in List") },
  { 1, Either.left("Head can only be used in List") },
  { {}, Either.left("Head can only be used in List") },
]
|> Enum.map(fn {input, except} -> TestHelper.executer(input, except) end)
[Success] [1, 2, 3, 4, 5] -> {:right, 1} == {:right, 1} is true
[Success] [] -> {:left, "Emplty List!"} == {:left, "Emplty List!"} is true
[Success] "" -> {:left, "Head can only be used in List"} == {:left, "Head can only be used in List"} is true
[Success] 1 -> {:left, "Head can only be used in List"} == {:left, "Head can only be used in List"} is true
[Success] {} -> {:left, "Head can only be used in List"} == {:left, "Head can only be used in List"} is true

全てのケースをpassしました。ElixirhaskellEitherを再現することに成功しました🎉
全体のコードはreplitから閲覧可能です。

replit.com

参考文献📚