Maybe 付きのパス

リストとパス」では、リスト構造を利用してパスを表現する方法について扱いました。また Maybe 型が、値を取得できる (できない) ことを示すために使えることも確認しました。

今回は、Maybe 型が複数現れるような場合でも、パス表現はまったく同じ形式で機能することを確認します。

ドメインモデル

銀行業務のドメインモデルに戻って、今回は銀行に 星付き の顧客 (ウォーレン・バフェットとでもしましょうか) がいる可能性があるとします。その顧客は、彼がお気に入りであるところのトップ運用成績のポートフォリオを保有しているかもしれません。この 星付き ポートフォリオには要チェックな、いわば 星付き ポジションがオプショナルな値として組み込まれています。

単純化された銀行業務ドメインにおける星
Figure 1. 単純化された銀行業務ドメインにおける星

このドメインモデルは、レコード構文を用いて、以下のように簡単に表現することができます。「リストとパス」でのデータ構造と似ていることに注意しましょう。異なるのは、型として現れていた リスト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

このアプローチでも動作はしますが、明らかに改善の余地ありです。

表記法の改善

ここは一度戻って、今回何をしているのかについて再考しましょう。

リストとパス」でやったのと同じように、登場する関数とその型を確認してみます。

Table 1. 関数とその型の組み合わせ
番号 関数

<1>

bank.star

Maybe Client

<2>

Clients.star

Client → Maybe Portfolio

<3>

Portfolio.star

Portfolio → Maybe Position

<4>

Position.ticker

Position → Ticker

各関数は、次の関数の引数の型に 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

嬉しいことに、すでに bind 関数が使える形になっていて、「お手軽入出力」や「リストとパス」と同じように >>= で記述することができます。

まず <1> と <2> を組み合わせると bank.star >>= Client.star

次に <2> と <3> を組み合わせると Client.star >>= Portfolio.star

そして <1> と <2> を組み合わせ、さらにそこに <3> を組み合わせると bank.star >>= Client.star >>= Portfolio.star

Important
ジャジャーン!
今回もシンプルな「パス」表現にたどり着きました。今回は、オプショナルな 星付き 顧客の、オプショナルな 星付き ポートフォリオの、オプショナルな 星付き ポジションに対するパスとなっています。

仕上げとして、一つのパスにまとめたものが以下です。

パス内の Maybe 値の連鎖
starTicker bank =
    bank.star >>= Client.star >>= Portfolio.star >>= \position -> Just position.ticker

Position.tickerMaybe 型だったらもっとすっきりと書けたでしょう。しかし銘柄がないポジションは存在し得ないため、こちらのほうがリアリティがあります。また、関数に渡す引数をラムダ式のパラメータとして束縛する例としても勉強になります。

型を確認するのは簡単です。「静かなる記号たち」ですでに見たように、

-- 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」記法が現れるのはごく自然なことです。

「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 を返します。

null は存在しない

ここで思い出しておきましょう。Frege に null は存在せず、したがって NullPointerException も存在しません。このことは何度でも繰り返し主張するに値します。

ただし、コードの見た目のみで比較できるわけではありません。

Frege では Maybe のコンテクストを取り回すことで、値が取得不可能かもしれないことを呼び出し側が忘れて、うっかり呼び出してしまわないことを型システムにより保証できる、という利点があります。

Java でも、もし仮に NullPointerException が「検査例外」であったなら (実際はそうではない) 同じような効果が期待できたでしょう。Java 8 以降には Optional 型が存在し、flatMap 関数がここで見た バインド と同じように動作します。この抽象化が Java でどれだけうまく機能するか、時が経てば明らかになることでしょう。

results matching ""

    No results matching ""