Maybe 付きのパス
「リストとパス」では、リスト構造を利用してパスを表現する方法について扱いました。また Maybe
型が、値を取得できる (できない) ことを示すために使えることも確認しました。
今回は、Maybe
型が複数現れるような場合でも、パス表現はまったく同じ形式で機能することを確認します。
ドメインモデル
銀行業務のドメインモデルに戻って、今回は銀行に 星付き の顧客 (ウォーレン・バフェットとでもしましょうか) がいる可能性があるとします。その顧客は、彼がお気に入りであるところのトップ運用成績のポートフォリオを保有しているかもしれません。この 星付き ポートフォリオには要チェックな、いわば 星付き ポジションがオプショナルな値として組み込まれています。
このドメインモデルは、レコード構文を用いて、以下のように簡単に表現することができます。「リストとパス」でのデータ構造と似ていることに注意しましょう。異なるのは、型として現れていた リスト が Maybe になったことと、それに合わせて名前を変更したことのみです。
data Bank = Bank { star :: Maybe Client }
data Client = Client { star :: Maybe Portfolio }
data Portfolio = Portfolio { star :: Maybe Position }
data Position = Position { soMany :: Int, ticker :: Ticker }
Note
|
後で取り回しが若干複雑になるのですが、Position は常に銘柄を保持しています。これはドメインモデルに気持ち「リアリティ」を残すためです。
|
それでは例として、オプショナルな値がすべて実際に値を持っている銀行を作ってみましょう。
starBank = Bank { star = Just Client { star = Just Portfolio { star = Just Position { soMany = 8, ticker = CANO } } } }
しかし他の銀行には、星付きの顧客がいなかったり、いたとしても優良ポートフォリオを保有していなかったり、保有していても特筆すべきポジションが組み込まれていなかったりするかもしれません。
noStarBank = Bank { star = Nothing } noPortfolioBank = Bank { star = Just Client { star = Nothing } }
星付き顧客のお気に入りの銘柄に投資して一山当てる、なんてことを企んでみましょうか。そのためにはまず、該当する銘柄を見つける必要があります。
はじめの一歩
星付きの銘柄を見つけるためには、星付き顧客 (もしいれば) の星付きポートフォリオ (もしあれば) の星付きポジション (もしあれば) を見つけなければなりません。
この目的を達成するために、starTicker
関数を作りましょう。この関数は bank
を引数に取り、もし星付きの銘柄が存在すればそれを Just
の中に入れて返し、存在しなければ Nothing
を返します。 要するに、以下のような型を持つ関数です。
starTicker :: Bank -> Maybe Ticker
これを直接実装する方法はいくつも (コンストラクタによる場合分け、case 式、if を入れ子にする、maybe 関数) ありますが、どれを選んだとしても、下に挙げる実装に似たごちゃごちゃした解法になります。
starTicker bank = starPortfolio bank.star where
starPortfolio Nothing = Nothing
starPortfolio (Just client) = starPosition client.star where
starPosition Nothing = Nothing
starPosition (Just portfolio) = starTicker portfolio.star where
starTicker Nothing = Nothing
starTicker (Just position) = Just position.ticker
これが期待通りに動作するかどうかを確認してみましょう。処理中のあちこちに Nothing が現れる可能性があり、その場合にエラーとならないよう保証しておく必要があるので忘れずに。
import Test.QuickCheck
star1 = once $ starTicker starBank == Just CANO
star2 = once $ starTicker noStarBank == Nothing
star3 = once $ starTicker noPortfolioBank == Nothing
このアプローチでも動作はしますが、明らかに改善の余地ありです。
表記法の改善
ここは一度戻って、今回何をしているのかについて再考しましょう。
「リストとパス」でやったのと同じように、登場する関数とその型を確認してみます。
番号 | 関数 | 型 |
---|---|---|
<1> |
|
|
<2> |
|
|
<3> |
|
|
<4> |
|
|
各関数は、次の関数の引数の型に Maybe をつけたものを返します。したがっておそらく、このパターンを一般化して、関数を バインド することで一行にまとめることができるはずです。
まず <1> と <2> をバインドすると
<1> <2> return type Maybe Client -> (Client -> Maybe Portfolio) -> Maybe Portfolio
次に <2> と <3> をバインドすると
<2> <3> return type Maybe Portfolio -> (Portfolio -> Maybe Position) -> Maybe Position
見ての通り、背後には以下のような型を持つ bind によって一般化されたパターンがあります。
Maybe a → (a → Maybe b) → Maybe b
まず <1> と <2> を組み合わせると bank.star >>= Client.star
次に <2> と <3> を組み合わせると Client.star >>= Portfolio.star
そして <1> と <2> を組み合わせ、さらにそこに <3> を組み合わせると bank.star >>= Client.star >>= Portfolio.star
Important
|
ジャジャーン!
今回もシンプルな「パス」表現にたどり着きました。今回は、オプショナルな 星付き 顧客の、オプショナルな 星付き ポートフォリオの、オプショナルな 星付き ポジションに対するパスとなっています。
|
仕上げとして、一つのパスにまとめたものが以下です。
starTicker bank =
bank.star >>= Client.star >>= Portfolio.star >>= \position -> Just position.ticker
Position.ticker
も Maybe 型だったらもっとすっきりと書けたでしょう。しかし銘柄がないポジションは存在し得ないため、こちらのほうがリアリティがあります。また、関数に渡す引数をラムダ式のパラメータとして束縛する例としても勉強になります。
型を確認するのは簡単です。「静かなる記号たち」ですでに見たように、
-- the type of the anonymous lambda expression is
-- Position -> Maybe Ticker
\position -> Just position.ticker
は単なる
foo :: Position -> Maybe Ticker
foo position = Just position.ticker
の別表記で、いい関数名をわざわざ考える労力を使わずに済ませることができます。
「do」記法、ふたたび
一方、バインドがあるところ、少し足を伸ばせば「do」記法が現れるのはごく自然なことです。
---
starTicker bank = do
warrenBuffet <- bank.star
starPortfolio <- warrenBuffet.star
starPosition <- starPortfolio.star
Just starPosition.ticker
---
かなりいい感じに読みやすくなりますし、まさに狙ったとおりに動きます。各ステップは Nothing
に評価され得ること、またその場合にはそれ以上評価を続けることなく 即座に Nothing
が返ることに注意しましょう。
アプローチの比較
Maybe
型は、パスによる表現を用いるにしろ do 記法と組み合わせて使うにしろ、どちらにしても応用が効きます。
他の言語であっても、パスによる表現が簡潔に書けることがあります。今回で言えば、例えば Groovy の GPath では bank.star?.star?.star?.ticker
となりますが、パス中のどこかが null となった場合には全体として null を返します。
ただし、コードの見た目のみで比較できるわけではありません。
Frege では Maybe のコンテクストを取り回すことで、値が取得不可能かもしれないことを呼び出し側が忘れて、うっかり呼び出してしまわないことを型システムにより保証できる、という利点があります。
Java でも、もし仮に NullPointerException が「検査例外」であったなら (実際はそうではない) 同じような効果が期待できたでしょう。Java 8 以降には Optional 型が存在し、flatMap
関数がここで見た バインド と同じように動作します。この抽象化が Java でどれだけうまく機能するか、時が経てば明らかになることでしょう。
参考文献
Groovy Null-Safe |
http://groovy-lang.org/operators.html#_safe_navigation_operator |
Learn you a Haskell | |
Java 8 Optionals |
http://www.oracle.com/technetwork/articles/java/java8-optional-2175753.html (possibly contains some errors) |