「for式による関数合成」は、Scalaを学び始めた方にとって難しく感じられるトピックの1つなのではないでしょうか。
この記事では、Scala初学者の方や教育を行う方を対象に、for式による関数合成を「線路」のメタファーを用いて理解・説明するアプローチを紹介します。
for式は「反復処理専用の構文」?
「for文」を持つプログラミング言語の経験がある方にとっては、Scalaの for式
はまず「反復処理のための構文」と捉えられることが多いのではないでしょうか。
例えば「Scala スケーラブルプログラミング」でも、「受け取った複数のコマンドライン引数をfor式で反復的に処理する例」がまず紹介されます。1
|
|
しかし、for式の役割は反復処理のみに留まりません。for式を反復処理の専用構文と捉えてしまうと、以下のコードは一見意味不明なものに見えるでしょう。
「ここで行っているのはfor式によるEitherの合成だよ」と一言で説明しても、Scalaを学び初めた人にとっては、なかなかピンとこないのではないでしょうか。
|
|
ここで行っていることを分かりやすく説明するには、どのようなステップを踏むのがよいでしょうか?
この記事では、Scott Wlaschin による “Domain Modeling Made Functional” を参考に、「線路」のメタファーを用いて説明をするアプローチを紹介します。
for式による関数合成を理解する3ステップ
for式による関数合成を「線路」のメタファーで説明するアプローチでは、以下の3ステップを踏んでいます。
- 「2トラックな関数」を理解する
- 2トラックな関数同士をどう組み合わせるかを理解する
- コードのネストを避けるためにfor式を利用する
それぞれのステップを順に見ていきましょう。
1. 「2トラックな関数」を理解する
現実世界のアプリケーションは Happy path を考慮するだけでは不十分です。たとえばECのアプリケーションであれば、注文された商品の在庫切れなどの理由によって期待されたシナリオを実行できない状態、 “Unhappy path” をハンドリングする必要があります。
“Unhappy path” は例外(Exception)として表現することも可能ですが、「例外的な状況にだけ例外を使用する」指針に従い、「商品の在庫切れ」のような業務上想定できるエラーは値 (OutOfStockError) としてモデリングすることとします。
例外とは何か例外的なもの(ある意味、あきらめ)を表現するものであるとした場合、失敗は想定できる結果であるため、その失敗を例外としてモデリングすることは意味的におかしいことになってしまいます。
— 『セキュア・バイ・デザイン』 Dan Bergh Johnsson、 Daniel Deogun、 Daniel Sawano
「Happy / Unhappy な結果のいずれかを返す関数」を表現するために、今回は返り値の型として Either
2 を利用します。
例えば「在庫を確認する関数」は以下のようになるでしょう。
|
|
その他の「注文をバリデートする関数」や「価格を確定する関数」も同様にEitherを用いて、Happy / Unhappy な結果をとりうるように表現してみます。
|
|
これで、注文を行うために必要な3つの「2トラックな関数」ができました。
これらの関数を組み合わせ、「いずれかの関数が Unhappy な結果を返した際には、その先の関数をバイパスするように合成したい」とします。
そのような合成はどう実現するのがよいでしょうか?
2. 2トラックな関数同士をどう組み合わせるかを理解する
そのためにあるのが flatMap
です。先ほどの3つの関数を、flatMapを用いて組み合わせると以下のようになります。
|
|
validateOrder
, checkStock
, priceOrder
の3つの関数をflatMapで繋いでいます。
こうしてできた placeOrder
関数は、UnvalidatedOrder
を受け取り、3つすべての関数が Happy な結果、つまりRightを返せばPricedOrder
を、いずれかの関数が Unhappyな結果としてLeftを返せば OrderError
を返します。
3つの関数の結果がすべて Happy であれば、通るルートは以下のようになります。注文のバリデート, 在庫確認, 注文金額の確定を無事通過してハッピーエンド、3つの関数を合成した結果としてRightで Priced Order
を返します。
仮に checkStock
が「在庫がない」と判断した場合は、その先の関数をバイパスして、OutOfStockError
をLeftで返します。
flatMapを用いることで Unhappy な際のバイパスを実現しつつ、関数同士を合成することができました。
一方で気になるのが、「インデントの谷」です。合成する関数が増えるほど、谷は深くなっていってしまうのでしょうか?
3. コードのネストを避けるためにfor式を利用する
この谷を平らにならすために使えるのが for式
です。上記のflatMap版のコードは、for式を用いると下記のように表現できます。
こうすると、合成する関数が増えても見通しは良いままですね。
|
|
for式は、flatMapやmapを用いたコードをより端的に書きやすくするためのシンタックスシュガーです。上記のfor式を用いたコードは、コンパイル時にflatMap等の呼び出しに変換されます3。
より簡単なコードを用いて、for式がflatMap等の呼び出しに変換されていることを確認してみましょう。このようなコードを用意し、ファイルとして保存します。
|
|
このファイルを scalac
で -Xprint:namer
のオプション付きでコンパイルし、どのように変換されているかを見てみましょう。
|
|
Right(2).flatMap(((a) => Right(3).map(((b) => a.$times(b)))))
を見ると分かるように、for式が flatMap や map の呼び出しに変換されていることが確認できました!
まとめ
この記事では “Domain Modeling Made Functional” を参考に、flatMapやfor式による「2トラック」な関数の合成を「線路」のメタファーを用いて理解するアプローチを紹介しました。
今回の例ではEitherを返す関数の合成を取り上げましたが、線路のメタファーはOptionやTry, Futureなど各種データ型の合成の理解にも同じように役立つのではないでしょうか。
このアプローチが、Scala初学者の方や教育を行う方の一助となれば幸いです。
-
後続の第23章「for式再説」では、for式の反復処理以外の機能についても触れられています。 ↩︎
-
Eitherとは、RightあるいはLeftの2つの値のどちらかを持つ型です。慣例的に、「正しい」(Right)にかけて、正常時の値がRight、エラー時の値がLeftとして表現されることがよくあります。 ↩︎
-
厳密には
yield
句の有無によって変換のされ方が異なります。Scalaコンパイラはyield
句 を含むfor式はmap
とflatMap
,withFilter
の呼び出しに、yield句を含まないfor式はwithFilter
とforeach
の呼び出しの組み合わせに変換します。 ↩︎