オブジェクト指向と関数型プログラミングは両立するのか?

オブジェクト指向と関数型プログラミングは両立すると言われる。実際に両方のパラダイムを持つプログラミング言語も存在し普及もしている。確かに言語というレベルでは両立しているように見えるが、果たしてプログラムデザインのレベルではどうなのだろうか? 関数型プログラミングに入門してから一年余り、どうもこの二つの考え方は両立しないのではないかという思いが強くなってきた。あるいは、これらが両立するという前提が混乱の元になっているのではないかと思うようになった。

例えば、「関数型つまみ食い: 関数型プログラミングの何が嬉しいのか?」では、オブジェクト指向と関数型での、状態に対するアプローチの違いについて触れた。オブジェクト指向では状態とその遷移を隠蔽することによってシステムを単純化する一方、関数型では(多少冗長になっても)状態遷移を明確にすることによってシステムの動作を予測しやすくするという対比だ。つまり両者は、状態に関しては真逆のアプローチを取っている。

オブジェクト指向でも、不可変なデータを扱うことによって意図しない状態遷移を防ぐことができるし、それによってオブジェクト指向でなくなってしまうわけではないと考えることもできる。あるいは、副作用を避ける(不可変なデータを扱う)場所と、副作用を扱う場所を分けることによって、両者の考え方をうまく共存させることができるのではないかと。

しかし、先月開催された Elm Tokyo Meetup にて教えて頂いた、Elm Europe 2017 のアーカイブ動画を見ていたら、オブジェクト指向と関数型はそもそも根本的に異なる思想なのだという思いを新たにした。ちなみに Elm は Web のフロントエンド開発に特化した、純粋関数型のプログラミング言語である。

この発表では、プログラムデザインの流れをもう一度基礎から再考すべきであるということが語られている。まずは一枚岩で構造化されてない状態から初めて、「Build -> Discover -> Refactor」というサイクルを回しながら、構造化すること自体を目的化せず、今行おうとしているそのリファクタリングによって「そもそも何を保障したいのか?」を問う。

ここでの狙いは、過去の経験によって培われた「構造化の先走り」を防ぐことにある。「構造化の先走り」とは何か? Elm で有名な問題に「コンポーネント・アンチパターン」というものがある。

以前の Elm のチュートリアルでは、UI をコンポーネントの集まりとして表現する、というそれまで当たり前に考えられてきた文脈に沿って、状態とその変更ロジック、そしてその状態を表示するためのビューをひとまとめにしたコンポーネントというものを定義し、それらを組み合わせることによってアプリケーションを構築するという手法を紹介していた。しかし、このデザインでは、アプリケーションが大きくなるにつれてコンポーネント同士を連携させるためのボイラープレートが膨大になったり、柔軟性に欠けるケースや無駄な重複が現れるとして、その後、コンポーネント化はできるだけ避けるべきだと明言されるようになった。

Elm Europe の動画では、コンポーネントとは結局のところ、状態と操作をひとまとめにする、オブジェクト指向の構造化手法であることが指摘されている。長らくオブジェクト指向に親しんできたプログラマーが Elm に同様の手法を適用した結果、思ったような柔軟性を得られなかったという流れだ。

このブログでは過去に「オブジェクト指向とは何だったのか?」という記事で、オブジェクト指向をその起源に遡って解釈しようと試みたことがある。

ケイ氏は、オブジェクトを、ネットワークを形成してメッセージを送り合うコンピュータのメタファーとして捉えており、インターネット上のサーバーのように、リクエストをメッセージとして受け取り、そのメッセージをサーバー側で解釈して何らかの処理を行うというモデルを想定していた。

この全てがオブジェクトであるという原則と、大きなシステム(オブジェクト)は小さなシステム(オブジェクト)の組み合わせで作られるという「再帰的デザイン」によって、どんな複雑なシステムをデザインするときでも、覚えなくてはならない原則は少なくて済むようになる。

オブジェクト指向では上のような再帰的構造を持つものとしてシステムを捉える。これがアプリケーションに応用されると「アプリケーションはより小さなアプリケーションの集合によって表現される」ことになる。凝集性の観点から言えば、これはとても魅力的なアプローチに思える。アプリケーションを構成する、より小さなサブアプリケーションは各々が独立していて、変更の影響も局所化される。

しかし、関数型の世界(少なくとも Elm の世界)では、このモデルに疑いの目が向けられることになった。

件の動画では、アプリケーションはアプリケーション固有の(そもそも必要とされる)複雑さを持つのだから、その複雑さに対応するために、データから操作を切り離し、両者をより柔軟に組み合わせることによってその複雑性に対応すべきだということが示唆されている。その結果として、Elm ではグローバルな状態やロジックを集めたコードが長くなることについて、他の言語よりも寛容である、ということが説明される。これはあるいは Elm の持つ強力な型システムの支えもあるかもしれないが。

アプリケーションの再帰的構造というオブジェクト指向のモデルは、昨今のフロントエンドのような複雑なシステムを表現するには単純に過ぎるという問題は、オブジェクトとリレーショナルモデルをマッピングするときに問題視されたインピーダンスミスマッチの問題に似ているかもしれない。

最近、この問題に関係していると思われる大変興味深い記事に遭遇した。Cindy Sridharan 氏による「小さな関数は有害だと考えられる」というタイトルの記事だ。

タイトルが若干ミスリーディングであることは本人も認めているが、彼女が言わんとしていることはこういうことである。名著「Clean Code」などに書かれていたり、あるいはソフトウェア開発における著名人が喧伝するような、今では当たり前とされる、オブジェクト指向時代に生まれた設計指針が果たして今も本当に有効なのかどうか今一度確認してみるべきではないのか、と。

彼女の問題意識は、適切な抽象化というものがそもそも難しいのだというところから出発して、DRY原則などによって無条件に構造化を推し進めようとする既存の設計手法は、多くの場合に失敗に終わる抽象化によって、可読性や柔軟性を欠いたコードになることが多いという発見に辿り着く。オブジェクト指向時代の多くのプラクティスは抽象化が適切に行われることを前提にしているため、物事が複雑に揺れ動く現実の世界では、そもそもそのメリットを享受することが難しい。

duplication is far cheaper than the wrong abstraction, and thus to prefer duplication over the wrong abstraction

そもそも完璧な抽象化というものは存在しない。抽象化された概念というのは、ある文脈を前提にした主観的なものに過ぎないからだ。そしてのその文脈はあらゆる要因によって常に揺れ動いている。Sridharan 氏は、既存のコードが持つ影響についても指摘している。すでに書き上がったコードはある種の成果であるため、たとえ文脈が変わってしまっても、そもそもの文脈を廃棄することには心理的な抵抗が働くのだと。その結果、いびつな形で増改築が行われた建築物のように、構造化はされているが、全体像を捉えるのは難しいプログラムが出来上がってしまう。

より速くそして複雑に変化するシステムを捉えるためには、無条件な構造化については慎重にならなければならない。下手に構造化するぐらいであれば、多少関数が長くなっても実直に書かれている方が遥かに可読性が高く修正もしやすい。そして、過剰な抽象化を避けるためにコードのライフサイクルについても思いを馳せるべきだと Sridharan 氏は指摘する。これまでの設計指針では、コードを追加したり、削除したりする際は Open-Closed Principle(開放/閉鎖原則)に則るべきだと言われてきた。しかし、その原則がうまく働くのは抽象化がうまく行われた時だけである。であれば、「修正しやすい」こととは何かということを今一度考え直すべきなのかもしれない。

さて、この議論を呼びそうな記事に対しては、当然のことながら、既存の設計指針を支持する陣営から反論が行われている。以下はその一例である。

反論の要旨は以下のような感じだ。

  • 大きな関数は変更が局所化されないので脆弱である
  • ユニットテストが書きづらくなるため、結果としてテストされないコードが増える
  • 適切に抽象化されれば、やたらと長い名前問題はそもそも起こらない
  • コードは Open-Closed Principle に従って修正されるべきである
  • 大きな関数は副作用を持つ可能性が高くなる

こういう反論はいかにもありそうだという印象を個人的には受ける。「あなたはそもそも適切に抽象化できていないから」小さな関数が有害だと感じるのだという、オブジェクト指向界では比較的馴染みのあるものだ。「そもそも抽象化が難しい」というところには応答しておらず、議論はすれ違っている印象を受ける。

「そもそもやり方が悪いんだ」問題は、以前このブログで紹介した「Is TDD Dead? 会談」にも現れていた。

DHH氏の想定するようなTDDは、Beck氏やFowler氏の想定するTDDとは異なり、単にやり方が悪いからうまく行かないだけで、DHH氏の発言は「わら人形論法」に過ぎないというのが、TDD肯定派の反論である。それに対して「うまく行かないのはやり方が間違えているからであって、正しく実践すればいつかは正解に辿り着ける」というのは「faith-based TDD」という信仰であって、そのようなものは認められないというのが DHH氏の立場であり、ここがすれ違いの中心になっている。

そして、「ユニットテストが書きづらくなる」という主張も、関数型(特に Elm のような静的型付けの)ではユニットテスト の役割が相対的に小さくなること、あるいは「TDD再考」でも取り上げたように、ユニットテストそのものの価値の問題を考えると、それほどクリティカルな反論であるとは言えないように思える。

関数型つまみ食い: 関数型プログラミングの何が嬉しいのか?

 

「モナド会」とは、モナドをまともに使ったことがない人間が、モナドどころか関数型プログラミングの経験もない人間に、モナドについて解説するという恐ろしい会である。

実は以前、モナドについての記事を書いたことがある。

モナドについての知識が全くない頃に(今でもかなり怪しいが)、Philip Wadler 氏の論文を読んで、少し分かった気になったので軽い気持ちで書いた記事だ。しかしその後、何の間違いなのか、「モナド」でググるとこの記事が1ページ目に表示されるようになってしまった。本当に申し訳ない気持ちでいっぱいである。

この責任ある立場としては、「モナド会」なるものを開催し、分かったつもりの勢いで初心者に解説を試みて、そしてその成果をここで紹介することでより混乱を深めていくしかない、そのように決意した次第である。

というわけで、今回は「モナド会」で説明を試みた話題の中から、最も根本的な話である「そもそもなんで関数型プログラミングが必要なのか?」というお題について紹介してみたい。

 

オブジェクト指向と不確定性

関数型プログラミングのメリットは、これまでの主流を占めていたオブジェクト指向プログラミングとの比較で考えると分かりやすい。

一言で言えば、オブジェクト指向と比較して関数型は「原因と結果を局所化するので、システムの動きが分かりやすくなる」。

どういうことだろうか?

以下の図を見て欲しい。

オブジェクト指向では、システムに何か動きがあったとき、その動きの原因となる箇所と、結果となる箇所が分散しているため、システムの動作(状態遷移)が把握しづらくなる。上図で言うと、青い部分が原因になる箇所で、赤い部分が結果として状態変更の行われる可能性のある箇所だ。

まず、青丸に Arg と書かれている method の引数が動作の入力になるというのは、比較的すんなりと理解できる。ところが、図をよく見ると、Devices と書かれた箱も青い線で囲まれていて、入力の一部になっていることが分かる。Devices は、プログラムの外部にあるサービスを表している。単にオブジェクトを利用するときには意識に上らないことが多いが、実は Devices の状態も事前条件として、動作に影響を与える「原因」の一部になっている。

さらに、結果の方を見てみると、動作に関係しているオブジェクトそれぞれの状態が変更される可能性がある上に、Devices にも状態変更が起こる可能性があることが分かる。

Devices を操作するのがプログラムのそもそもの目的なのだから、当たり前と言えば当たり前の事態なのだが、オブジェクト指向言語でユニットテストを書いたことがある人なら、なんとなくこれらの厄介さが分かるのではないだろうか。

とあるメソッドのテストを書く場合、単純に引数を渡して実行すればOKというわけには行かず、依存オブジェクトやシステムについて、何らかの準備が必要になることが多い。そして、結果を検証する際にも、オブジェクトの境界だけを確認するか(Mockist Testing)、あるいは分散したシステムの状態を確認するか(Classical Testing)といった選択に悩むことになる。

このように、プログラムから直接接続された Devices(外部サービス)は、プログラムの挙動を予測しづらくする諸悪の根源なのである。

 

純粋関数 – 原因と結果の局所化

関数型では、この原因と結果を、関数の入出力として局所化するため、システムの動きが格段に分かりやすくなる。

この原則が徹底されているとき、つまり、システムで発生し得る状態遷移の全てが関数の入出力として表現されてるとき、これらの関数を純粋な関数と呼ぶ。

純粋な関数だけで構築されたシステムでは、その入出力として表現されている以外の出来事は起こらない。つまり、関数の入出力を見ればシステムがどのように動くかを完全に把握できるということだ。

そのようなシステムを図にしたのが以下である。

システムが動くときの原因と結果が、関数の入出力として局所化されていることが分かる。

これでシステムの状態遷移がシンプルになったし、めでたしめでたしと言いたいところだが、オブジェクト指向で Devices にアクセスしてた部分はどうなったのだろうか? Devices を操作できなければ、まともなプログラムは作れないはずである。

実はそこに純粋関数型プログラミングのトリックがある。

図をよく見ると、関数は入力を得て出力を返すのみで、Devices へ直接アクセスすることはないものの、入出力を受け渡しするレイヤーとして Runtime というものが Devices との間に挟まっている。

純粋関数型は、Devices を直接操作できない代わりに、Devices への命令をデータとして出力し(図中の出力に Command が含まれていることに注目)、それを Runtime に渡すことによって、間接的に Devices を操作する。

このようなややこしい迂回をすることによって、外部サービスをプログラムから切り離して、関数の純粋性を保つわけである。

Devices への命令をデータ化して、プログラム全体を純粋関数にしようとするのは、関数型プログラミングの中でも最もハードコアな部類になるとは言え、基本的に関数型プログラミングは、純粋関数を出来るだけ多く導入することによってシステムから不確定性を取り除こうとする考え方だと言って良いのではないだろうか。

純粋関数にはメリットがある。しかし、それを徹底しようとすると「命令をデータとして表現する」というややこしい方法を取らざるを得ない。その結果、命令型の言語のような簡潔さは失われることになる。その失われた簡潔さを取り戻すために「モナド」のような仕組みが活躍することになるのだが、これはまた続きの記事で紹介したいと思う。

Elixir/Phoenix と Elm による関数型 Web 開発環境の構築

前回は、Cotoami のアーキテクチャについて、コレオグラフィ型を採用するという話を書いた。しかし、開発の最初からコレオグラフィを前提にした構成にするのはスモールスタートとは言い難いので、まずは核となるWebアプリケーションを作るところから初めて、徐々にイベント駆動の箇所を増やしてく感じで進めたい。

このWebアプリケーションを実装する環境として選んだのが、Phoenix FrameworkElm である。両方とも関数型の言語なので、Webアプリケーション全体を関数型の枠組みで実装することになる。

Elixirの強みについてはゆびてくで何度か触れているのでここでは割愛するが、Elm を選択したのは何故だろうか?

大きな要因としては、Elmアプリのアーキテクチャを参考にデザインされたという JavaScript のライブラリ Redux での開発経験が挙げられる。その過程で、複雑化するフロントエンドを実装する技術として、全てのビジネスロジックを「変換の連鎖」へと落とし込む関数型の有効性を実感した(参考: 関数型つまみ食い: 関数型とはプログラミング言語ではなく、プログラムデザインの問題であることに気づく | ゆびてく)。Elm の場合は、Redux では冗長になりがちだったこの仕組みを簡潔に表現出来る上に、Static Typing があるというのも大きなアドバンテージだと考えた。

エディタ上で即座にフィードバックを受けることが出来る
プログラムの誤りについて、エディタ上で即座にフィードバックを受けることが出来る

Phoenix と Elm の相性については、最近 Elm 側で Phoenix のサポートが入ったというのが明るい材料ではあるが… こればかりは試してみないと分からない。


[2016/12/09追記]

素晴らしいツッコミを頂く。


 
以下に Phoenix/Elmアプリケーションのひな形を作るまでの手順をまとめてみた。

 

関連ツールのインストール

Node.js

以下を参考に nvm をインストールする。

creationix/nvm: Node Version Manager – Simple bash script to manage multiple active node.js versions

  • Phoenixのサイトに「Phoenix requires version 5.0.0 or greater.」とある。

筆者の環境:

$ node -v
v5.4.1

 

Elixir

Installing Elixir - Elixir

Mac OS X で Homebrew を利用している場合。

$ brew update
$ brew install elixir

筆者の環境:

$ elixir -v
Erlang/OTP 19 [erts-8.0.2]  [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.3.4

 

PostgreSQL

標準構成の Phoenix が利用するデータベース。環境によってパッケージも様々なのでインストール方法については割愛。データベースを使わないのであれば省略可。

以下のコマンドでデータベース一覧が取得出来ればデータベースのスタンバイは出来ている。

$ psql -l

筆者の環境:

# SELECT version();
                                                              version                                                              
-----------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 9.4.0 on x86_64-apple-darwin13.4.0, compiled by Apple LLVM version 6.0 (clang-600.0.56) (based on LLVM 3.5svn), 64-bit
(1 row)

 

Phoenix

Installation · Phoenix

$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez

筆者の環境:

$ mix phoenix.new -v
Phoenix v1.2.1

 

Elm

インストーラが用意されているので簡単。

Install · An Introduction to Elm

筆者の環境:

$ elm -v
0.18.0

 

Phoenix/Elm アプリケーションを作る

Phoenixアプリのひな形を作る

$ mix phoenix.new cotoami

依存関係の取得とデータベースの作成。

$ cd cotoami
$ mix deps.get
$ mix ecto.create   # PostgreSQLを使わなければ省略可
$ npm install

アプリを起動してブラウザでチェックしてみる。

$ mix phoenix.server

http://localhost:4000 にアクセスすると「Welcome to Phoenix!」のページが表示される。

 

elm-brunch をセットアップする

Phoenix に標準で付いてくる Brunch というJavaScriptのビルドツールがあるのだが、elm-brunch という Elm をビルドするための拡張があるのでそれをインストールする。

$ npm install --save-dev elm-brunch

brunch-config.js に elm-brunch の設定を追加。以下の二カ所を修正。

  1)
    ...
    watched: [
      "web/static",
      "test/static",
      "web/elm"
    ],
    ...

  2)
  ...
  plugins: {
    elmBrunch: {
      elmFolder: "web/elm",
      mainModules: ["App.elm"],
      outputFolder: "../static/vendor"
    },
    babel: {
      // Do not use ES6 compiler in vendor code
      ignore: [/web\/static\/vendor/]
    }
  },
  ...

 

Elmアプリのひな形を作る

$ mkdir web/elm && touch web/elm/App.elm
$ cd web/elm
$ elm package install elm-lang/html

App.elm の内容を以下のように編集。

module App exposing (..)

import Html exposing (Html, text)

main : Html msg
main =
  text "Hello Cotoami!"

 

ElmアプリをPhoenixアプリに配置する

Phoenixアプリのファイルをそれぞれ以下のように編集。

web/templates/layout/app.html.eex


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Hello Cotoami!</title>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
</head>
<body>
<div class="container">
<main role="main">
<%= render @view_module, @view_template, assigns %>
</main>
</div>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>

view raw

app.html.eex

hosted with ❤ by GitHub

web/templates/page/index.html.eex

<div id="elm-container"></div>

web/static/js/app.js に以下の二行を追記:

const elmDiv = document.querySelector("#elm-container")
const elmApp = Elm.App.embed(elmDiv)

これで準備は完了。ブラウザをリロードすると「Hello Cotoami!」と表示される。さらには、App.elm の内容を編集して保存すると、ブラウザが自動的にリロードされて即座に変更を確認出来るようになっているはずだ。

参考: Setting up Elm with Phoenix – Medium

関数型つまみ食い: 関数型とはプログラミング言語ではなく、プログラムデザインの問題であることに気づく

関数型プログラミングに精通していないプログラマにとって、「関数型」という言葉でまず思い浮かべるのは、Haskell や Erlang,Scala のような関数型言語や、Immutableなデータ構造、高階関数、ラムダ式、モナドと言った関数型プログラミングの道具立てだったりすることが多いのではないだろうか?

前回登場したゲームプログラマの James Hague 氏も、筆者と同じく、キャリア半ばで関数型の世界に足を踏み入れた越境者の一人だったが、その彼が、関数型言語 Erlang でのゲーム開発を経験して気がついたことがあると言う。それは、アプリケーション開発に純粋な関数(副作用のない関数)というコンセプトを導入することは、いわゆる関数型言語や関数型プログラミングとは全く関係ないということであった。

関数型というコンセプトの要は、ビジネスロジックを「データの変換」と捉えることにある。

functional1

関数というのは要するに「変換のルール」のことであるが、この関数が文字通り変換のルールのみを定義し(副作用がなく)、そして変換対象のデータを直接編集せずに、常に変換後のデータに丸ごと置き換えて行けば、システムの状態変化は常にアトミックに行われることになる。

functional2

このような形で、システムの状態遷移をシンプルにすることによって得られるメリットの大きさに、筆者の場合、関数型言語というよりもむしろ、Redux というフロントエンドのフレームワークを学ぶ中で気づかされた。

Redux はフロントエンドの状態管理を行うためのフレームワークである。オブジェクト指向に慣れているとアプリケーションの状態は個々のオブジェクトに分散するのが当たり前であるため、何故状態だけを独立して管理する必要があるのか、今ひとつピンと来ないこともあるかもしれない。GUIとオブジェクト指向というのは歴史的にも結びつきが強いため、この領域に関数型を適用するというのはどういうことなのか、これは筆者にとっても長年の疑問であった。

Redux は React というフレームワークとペアになっている。React はユーザーインタフェースをレンダリングするためのテンプレートエンジンで、入力データをテンプレートに当てはめて結果を出力するという、まさに関数としてデザインされている。そのため、テンプレート内で状態管理をすることは推奨されていない。状態は外部で管理して React 側に提供する必要がある。その状態管理を担うのが Redux である。

redux

Redux の仕組みは、上に書いたような関数型の状態管理そのままである。筆者にとって重要だったのは、これが JavaScript という、比較的慣れ親しんだ言語の上に構築されたフレームワークであったことだ。これによって「ああ、つまり関数型というのはフレームワーク(デザイン)なのだ」という気づきを得ることが出来た。

関数型デザインによって実現するシンプルな状態遷移のメリットは、Redux のプラグイン redux-logger によって出力されるログを見ると分かりやすい。

redux-logger

上のように、Redux で状態を管理していれば、そこで発生する全ての状態遷移は、

  1. prev state
  2. action
  3. next state

の繰り返しによってアトミックに行われる。何か問題が発生した時は、開発者自身があちこちにログを仕込んだりデバッガを活用せずとも、この状態遷移の内容を見れば何が起きたのか一目瞭然である。

Redux のドキュメントでは、関数型(functional)という言葉を出来るだけ使わずに説明を試みているようである。しかしながら、Prior Art にあるように、Redux の直系の先祖はブラウザで動く(ほぼ)純粋な関数型の環境 Elm であり、Elm は Haskell をベースにデザインされていることから、Redux は純粋関数型の思想を純粋関数型でない言語の上に実現したフレームワークだと言って差し支えないだろう。

関数型つまみ食い: モナドが難しいと思われている理由

(「モナドについて書かれたものを読む度に自分で書いてしまいたくなる。同じ過ちを繰り返すだけだから止めた方がいいんだけど。」という話を受けて、)

世の中に溢れる粗悪なモナド入門のせいで Philip Wadler氏の論文が埋もれているからモナドが難しいと思われるんだ。

ほほう、その論文を読めばモナドとやらを簡単に理解出来るのかな、と思いつつ読んでみると、

全然簡単じゃないんですけど…

でも、この論文でモナドの意義と基本的な仕組みはぼんやりと見えたような気もしなくはない。その理解をここに簡単にまとめておくことで、同じ過ちを繰り返してみることにしよう。

まずモナドを理解するためには、関数型プログラミングにおける、純粋な関数とそうでないものの区別をきちんとさせておく必要がある。

純粋な関数とは何か?

そもそも関数とは何か? それは、入力を出力に「変換」するものである。

関数: 入力 =(変換)=> 出力

至ってシンプルである。関数型プログラミングでは、この関数を組み合わせて、変換の連鎖としてアプリケーションを表現する。

さて、この関数が以下の二つの条件を満たすとき、その関数を「純粋」な関数と呼ぶ。

  1. 同じ入力に対しては常に同じ出力を返すこと
    • 例えば 1 を渡したら 2 が返ってくる関数があったとして、どのようなタイミングや事前条件で関数を実行しても 1 => 2 という同じ結果になること、そしてそれが他の入力パターンについても同様に成り立つこと。
  2. 関数は入力を出力に変換するものだと説明したが、その変換以外のことが一切起こらないこと
    • この変換以外の出来事を「副作用」と呼ぶ。

純粋な関数のメリットと痛み

Elixir試飲録 (4) – オブジェクト指向と関数型の違い」で触れたように、オブジェクト指向ではシステムの構成要素であるオブジェクトの「インターフェース」にフォーカスすることによって、システムの表現を単純化し、さらには疎結合による柔軟性を獲得していた。しかし一方で、インターフェースに現れない内部的なデータの流れは隠蔽されるため、インターフェースだけでシステムの(厳密な)状態遷移を把握することは困難になる。

object

少ない記述で多くを実現しようとするオブジェクト指向に対して、関数型、特に純粋関数型では、原理的に関数(入出力の変換)として書かれたことしか起こらないので、不測の事態が起こりづらく、より信頼性の高いシステムを構築しやすいと言える。しかし、逆に言えば、全てのデータの流れを入出力の変換として明示的に表現しなければならないので、時にはコードが冗長になってしまうケースがあるだろう。

function

この関数型の欠点とも呼べる冗長さを軽減するために導入されたのが「モナド」と呼ばれるコンセプトである。

モナド概念の導入

「関数型の欠点とも呼べる冗長さ」とは具体的にどのようなものだろうか? 以下のような例で考えてみる。

ここに「倍返し」という関数がある。名前の通り、受け取った数値を倍にして返す関数である。

倍返し: 数値 =(変換)=> 数値

この関数に 1 を渡すと 2 が返り、2 を渡すと 4 が返る。

この関数が「受け取った数値を倍にする」ことだけに専念しているとき、この関数は純粋であると言える。

しかし、世知辛い現実世界では、単に「倍返し」の純潔を守っているだけでは生き残って行けない。色んな要求が舞い込んでくる。例えば、倍返しに失敗したらどうしてくれるんだ?、だったり、倍返ししたことをちゃんと記録しておけよ、等々。そこで以下のような要求があった場合、どのように対応すれば良いだろうか?

  1. エラーが発生したら、それをきちんと知らせて欲しい
  2. 何回「倍返し」したのか記録しておいて欲しい
  3. 倍返しする度にログを吐いて欲しい

もし、関数が純粋でなくても良いなら、話は簡単である。エラー処理については、出力以外にも「例外」という経路を設けて、そこからエラーを知らせればよい。実際に多くのプログラミング言語ではこの方式を採用している。倍返し回数の記録についてはグローバルな変数を用意してそれをカウントアップして行けばよいし、ログについては深く考えることなく、普通にログを吐けばいい。

しかし、関数を純粋に保ったまま、つまり純粋な関数のメリットを維持したまま、このような機能を実現するためにはどうすればよいだろうか? 単純に考えると、出力にそのような情報を追加するしかない。

倍返し1: 数値 =(変換)=> 数値, エラー
倍返し2: 数値 =(変換)=> 数値, 回数
倍返し3: 数値 =(変換)=> 数値, ログ

エラーを投げたり、回数を記録したり、ログを吐いたりするのは「副作用」に当たるので、関数の純粋性を守るためには、これらの処理は「予定」として、関数の出力に含めて表現することになる。

これは明確さという観点から言うととても分かりやすい。関数の定義を見ればどのようなことが起こるのかが一目瞭然だからである。しかし一方で、関数の定義に、本来の処理「倍返し」以外の情報が紛れ込んでくるし、要求が増える度にいちいちこのような関数のバリエーションを増やして行かなければならないのだろうか?

そこで、このような「入出力に紛れ込む副作用」を本来の処理から隠蔽して冗長さを軽減するために導入されたのが、いわゆる「モナド」である。

「倍返し」の例を一般化すると、

倍返し: 数値 =(変換)=> 数値, おまけ

となるが、この関数に、どのような「おまけ」があるにしろ、そのことを意識せずに一番純潔な

倍返し: 数値 =(変換)=> 数値

として扱えれば便利である。そのために必要となるのが以下の仕組みである。

  1. まず前提としておまけ付きの出力: [本来の出力, おまけ]
  2. 本来の出力をおまけ付きに変換してくれる仕組み: 出力値 => [本来の出力, おまけ]
  3. おまけ付きの値を、そのままおまけ無しの値として関数に渡せる仕組み: [本来の入力, おまけ] => 本来の入力

この三つの組み合わせを「モナド」と呼ぶ。これらの道具立てがあれば、関数本来の計算がおまけに埋没することなく、かつ関数の純粋さを保つことができるというわけである。


[2017/05/18追記]

久しぶりにこの記事を読んでみて、もう少し説明を加えればモナドに対する理解がよりクリアになるような気がしたので、試しに書き加えてみる。

モナド三兄弟

関数型プログラミング(特に純粋関数型)は、システムの状態遷移を余すところなく関数の入出力として表現することで、システムの動きを明確にしようとする。全てを入出力として表現するので、ビジネスロジックにとって重要な「本来の入出力」以外の「おまけの入出力」がどうしても必要になってくる。

つまり、関数と関数の間を受け渡されていくデータには、常に何らかのおまけが含まれている可能性があるということだ。そのおまけは、例えば、処理に失敗した際のエラー情報だったり、値が存在しないかもしれないという可能性だったり、あるいは入出力には現れない文脈的なデータだったりする。このおまけの応用範囲がとても広いというのが、モナドのつかみどころのなさの一因になっていることは間違いなさそうだ。

しかし、関数の方はこのおまけを解釈するようにはできていないかもしれない、あるいはむしろ、特定のおまけを想定して関数を書いてしまうと、その関数の汎用性が大きく損なわれてしまうので、できれば本来必要な計算だけに集中したい。

そこで、おまけを想定しない関数を、おまけ付きの入出力を持つように変換するアダプター的な仕組みがあれば、本来の計算とおまけを切り離せる。この仕組みがモナドである。

より正確に言うと、この変換には三つのタイプがあり、その中の一つがモナドと呼ばれている。

具体的には、

a) 値 =(変換)=> 値  (全くおまけを想定してない関数)
b) 値 =(変換)=> 値, おまけ (おまけ付きの値を返す関数)
c) (値 =(変換)=> 値), おまけ (関数自体におまけが付いてる!?)

のような関数を、あたかも、

値, おまけ =(変換)=> 値, おまけ

のように扱えるようにしたい。この変換に対応するアダプターをそれぞれ、

a. Functor (ファンクタ)
b. Monad (モナド)
c. Applicative (アプリカティブ)

と呼ぶ。

ところで、上記三つの名前を見て、それぞれの機能が想像できるだろうか? これらは数学の「圏論 (category theory)」という分野由来の言葉で、そのままで意味をイメージできる人はかなりの少数派ではないかと思う。この名前の問題も、モナドが難しいというイメージを与える一因になっている。

そこで、以下のようにもっと分かりやすい名前で呼ぶようにしたらどうかという提案がなされているようだ。

  • Functor -> Mappable
  • Monad -> Chainable/Joinable/FlatMappable
  • Applicative -> Zippable

厳密にはこれらの語が意味するところとは異なるので(圏論的なニュアンスが抜け落ちるので)適切ではない、というのが専門家の見解であるようだが、これらの概念を理解する際の参考になることは間違いない。

まず Functor の Mappable という別名はとても分かりやすい。おまけ付きの値でも、[値 =(変換)=> 値] の関数でマッピングできるようにしてくれるものが Mappable というわけだ。

Functor で最もポピュラーだと思われるものが、値が「繰り返し」というおまけを持つ場合(これはつまりコレクションのこと)に対応する Functor だ。この Functor は、まさに map という名前で、多くの言語に導入されてお馴染みになった。コレクションを別のコレクションに変換するあの関数だ。この map に渡される変換用の関数は、値がコレクションの中に入っているという「おまけ」を気にすることなく、単に一つの値を別の一つの値に変換することだけに集中することができる。「本来の計算とおまけの分離」である。

Monad の Chainable も分かりやすい。

値 =(変換)=> 値, おまけ

という関数をつなげて (Chaining) 使いたい場合に、入力を変換するものが Chainable になる。

Applicative – 複数の入力値を取る関数をおまけ付き入出力に対応させる

Applicative はちょっとややこしい。これは、複数の入力値を取る関数に Functor 的な変換を行う際に使う。これまでの話では、あくまで一つの入力値を取るケースだけを想定して話をしてきた。

例えば、以下のように二つの入力を取る関数があるとする。

A, B =(変換)=> C

関数型プログラミングをサポートする言語の一部には、関数の「部分適用」という仕組みがある。これは複数の入力を受け取る関数があるときに、その中の一つの入力だけを渡して、その入力を受け取った状態の新たな関数を作る機能だ。

具体的に見てみよう。上の関数に、一番目の入力、A だけを与えると、入力を B 一つだけ受け取り、C を返す関数ができる。

A
↓
A, B =(変換)=> C
↓
B =(変換[A入力済み])=> C

さて、この部分適用という仕組みを踏まえて、二つの入力を取る関数におまけ付きの値を渡すためにはどうすれば良いだろうか?

まずは一つ目の入力値を Functor で渡してみよう。

A, おまけ
↓
A, B =(変換)=> C
↓
(B =(変換[A入力済み])=> C), おまけ

Functor は、入出力をおまけ付きに変換するので、部分適用して出来上がった関数がおまけ付きになってしまった。

というわけで、ここで Applicative の登場である。

すでに書いたように、Applicative は、

c) (値 =(変換)=> 値), おまけ

値, おまけ =(変換)=> 値, おまけ

に変換してくれる。つまり、

B, おまけ
↓
(B =(変換[A入力済み])=> C), おまけ
↓
C, おまけ

となって、最終的に欲しかった「C, おまけ」を手に入れることができた。

つまり、これら(Functor と Applicative)を組み合わせると、

A, おまけ and B, おまけ
↓
A, B =(変換)=> C
↓
C, おまけ

という感じで、複数の入力値を取る関数をおまけ付き入出力に対応させることができる、というわけである。