ガジェット通信

見たことのないものを見に行こう

JavaScript関数、Reactive Extensions(Rx)など、話題の関数型プログラミングを徹底解説!

DATE:
  • ガジェット通信を≫

関数型プログラミングは手続き型と同じぐらい古くからある

最初に登壇したのは、日本マイクロソフトのエバンジェリスト、荒井省三さん。「実践F♯関数型プログラミング入門」の著者であり、この本を元に、関数型プログラミングの解説が行われた。

関数型プログラミングは、手続き型と同じぐらい古くからある言語だ。英語で手続き型は「Imperative paradaigm」と呼ばれる。

一方、関数型は論理プログラミングを含めて宣言型パラダイム「Declarative paradaigm」と呼ばれる。つまり宣言型パラダイムには論理型パラダイムと関数型パラダイムの2つがある。

ちなみにPLANERやPrologは論理型パラダイムに属する。プログラムパラダイムがなぜできたかというと、何かの作業をさせたいという手続きや命令を書く作法が違うからだ。

日本において宣言型(関数型と論理型)言語はどのように活用されてきたのか。

1982年に当時の通産省による国家プロジェクト「知識情報処理指向の第五世代コンピュータ・プロジェクト」をきっかけに、関数型言語や論理型言語の専用コンピュータが開発されるようになった。

当時から宣言型パラダイムを実現したプログラミング言語は、非常にコンピュータリソース、パフォーマンスを必要としていた。

そのため、専用機の開発という研究者の世界でしか使われてこなかったのだ。

一般の人たちが使うようになったのは、インテルのCPUが出てきてからである。JavaScriptは関数型の中にオブジェクト指向の考え方をミックスした言語だ。ただ、ハイブリッドなので、関数型では不要なif文やfor文を使う。

例えば手続き型言語の一つ、C言語では最初に覚える処理にリスト処理がある。この処理があるのは、C言語が集合を扱う方法がないからだ。

一方、例えばLispという言語は、LIST処理を目的に開発された言語であるというように、関数型の言語は最初から集合を扱う前提になっている。

前述したようにfor文も関数型では使われない。また関数型ではifは用意されているが、if文ではなく「ifファンクション」として使う。

関数型でいうところの関数は、何かの引数を取って必ず値を返すということだからだ。一般的にCやJavaではVoidが使われる。

これを関数型のパラダイムで考えると、Void型となり、Voidという値を返しているという考え方をする。またVoidに相当するものとして、unitを設けており、値を返さない場合はunitを返すのである。

こうすることで、関数の基本である「何らかの引数をとって必ず値を返す」ことを外さないようにする。

基本から外れないというメリットは、コンパイラが文法チェックを100%できること。例えば手続き型ではif文を書かないことができるが、関数型ではそれを許されず、コンパイラはエラーを返す。

つまり、関数型の方が厳格にプログラムを書くことになるので、バグの混入が非常に少なくなるのだ。

関数型言語はモジュール化の概念を標準で持っていることも特長の一つ。よく、「良いプログラミングはモジュール同士の強度を低くすること」と言われているが、それをさらにモジュール分類しようというのがモジュール化の概念である。

JavaではModule Specification、JavaScriptも最新のECMAScript 6ではモジュールの概念が入ってくるが、関数型言語ではそれを標準で実現する。

関数型の特徴「イミュータブル」。このメリットとは?

関数型言語で慣れないのは、イミュータブルという考え方だ。例えばOCamlというオブジェクト指向をミックスした言語がある。

この言語でオブジェクトを作ると書き換えができるようになっているが、関数型言語なのでプロパティでアクセスできるのは読み出すだけである。

そこでプロパティを変えたいときは、新しいインスタンスに作り替えるということで書き換えできるようになる。新しいインスタンスを作って古いものを捨てるので、関数型言語の特長のイミュータブル性が保たれるというわけだ。

イミュータブル性にはいろんなメリットがある。例えば最近のコンピュータはマルチコアだが、処理を並列実行させるとしばしば、デッドロックが起こる。これはすべての処理は同じメモリを共有している状態で、リードライトを繰り返すことに原因がある。リード時にデッドロックに陥ってしまうのだ。

しかしイミュータブルだと読み出すだけになるので、デッドロックが現れない。ただし、これには前提条件がある。それはメモリを含めてコンピュータリソースが豊富にあること。

つまりこれこそが、過去にLisp専用機が作られてきた理由なのだ。この辺りのことをアカデミックの場で発言すると、「違う」と宗教論争に巻き込まれるので、注意してほしい(笑)。

もう一つ、関数型でよく宗教論争のネタになるのが、lazy evaluation。関数型では評価をした結果、関数を返すので、関数型では遅延実行ではなく遅延評価と訳される。

高階関数はその段階で関数そのものを実行せず、次のどこかの段階でその関数を実行するという関数はである。その関数を評価した結果、値が返る。これが遅延評価である。遅延評価は言語処理系によって実現方法が異なる。

関数型で学んでほしいことの第一は、関数型が引数をとって値を返すという大原則である。第二にこの引数の値を入れると同じ答えが返るという性質。

この性質は参照透過性と呼ばれる。これをプログラミング用語ではside effect(副作用)がない。副作用がないということは、どこのCPUを実行しても同じ値を返す。つまり並列に使うことができる。副作用があるとバグの温床になりやすい。

つまり関数の基本原則を意識して作ると、どんな言語でもハッピーになる機会が増える。このように関数型は勉強すると学ぶことが一杯ある。

古くからあるのになぜ、一気にメジャーにならないのか。それは話したとおり、パラダイムが違うからだ。今までの取り組みで取り組むと失敗するので留意してほしい。C#にはラムダ式がある。Linqで拡張メソッドを書くと、パイプラインのように書いていける。

これはデザイン的にはオブザーバーパターンとなっており、このアーキテクチャーをパイプラインアーキテクチャーという。そういう風な考え方ができると、リアクティブも含めて、わかりやすくなる。このように目線を変えて勉強していけば、関数型をハッピーに学べるはずだ。

関数は映画「INTERSTELLAR」で登場したワームホールのようなもの

続いて登壇したのは、Mozilla Japanの清水智公さん。言語処理系が大好きだ。

セッションタイトルは「JavaScript 関数プログラミング Y= f => (x => f(x(x)))(x => f(x(x)))」。

「INTERSTELLAR」という2014年に公開された米国のSF映画がある。地球が滅亡の危機をきっかけに、ワームホールを通過し居住可能な荷重惑星を探しに行く話の映画だ。ワームホールとは、そこを通ると別の空間にワープできるという空間領域。

ここで一旦、映画の話はおいて、関数の話へ。プログラムには2つ大事なモノがある。それは「操作」と「データ」である。手続き型言語は操作を中心にしたもので、いわば双六のようなものだ。

一方、データを中心に考えてみようというのが関数型言語である。関数は図のように2つの集合があってその間の関係を表す。

変数に依存して決まる値、もしくはその対応関係のことだ。つまり2つの空間を行き来している。つまり関数は先の映画における一種のワープホールみたいなものと捉えることができる。

JavaScriptは手続き的な書き方をするが、言語の成立時には関数型の言語の影響を多分に受けている。関数型言語の特徴は次の通りだ。

もちろん例外もある。例えばHaskellは型が強いが、型のない関数型言語もある。また参照透過性のないものもある。

いろいろな考え方もあるが、JavaScriptは関数をモノとして扱えるということ(第一級関数)とconstを利用すれば多少、単一代入的なこともできる特徴を持つ。

関数定義の仕方は次のようになる。function文では名前付きの関数を定義、function式では名前を付いていない関数(無名関数)を作ることができる。

以下はよく見る無名関数の利用例だ。

式とは評価すると値になるものである。無名関数の定義も式なので、関数を変数に代入できる。モノに代入できると言うことは、いろんな変数に代入し、関数を呼び出せる。

JavaScriptにおける関数は「1、2…」「あいうえお…」「true」「false」とまったく変わらないということ。つまり関数の別の関数の引数に渡すことも当然できる。これは関数型の世界では、高階関数と呼ばれる。

例えば合計値を求める関数を少し書き換えれば二乗和、三乗和を求める関数になる。これら3つのプログラムはほぼ同じである。

関数呼び出しをまとめるメリットは、プログラムがきれいになること。そしてJITコンパイルが早くできるようになることだ。

リスト処理は関数を使って美しく書く

また、関数を切り替えることで振る舞いを変更するプログラムが容易に書けるようになる。これが生きてくるのがリスト処理である。

「filter」という関数でフィルタする判断を、sortという関数では順序の判定方法を、「map/reduce」関数では、写像方法と左畳み込みの方法を、「every/some」関数では条件の判定を、「forEach」関数では繰り返す手続きをパラメータ化できる。

このように関数をモノとして扱えることは、フレームワークも作りやすくなる。

関数を関数で返すこともできる。関数を返す関数の利用例としては、引数の片方を固定し、特殊な関数を作成するとき。関数を返す関数を利用して、「同じようなことをする」関数を生成できる。

関数を関数で返すと、同じような処理のバリエーションを作る場合にまとめられるので、プログラムが美しく書ける。

また関数を引数とし、関数を返す関数を使えば、関数を切り替えることで振る舞いを変更することができる。

例えばリストの総和を求めるといっても、二乗和や三乗和など様々な和が存在する。通常は個別に関数を定義することになるが、関数を返す関数を適切に利用することで重複したコードを持つ複数の関数を個別に定義するなとを避けられる。

関数型言語の特徴である、参照透過性。これはエクセルのようなもので、1個のセルが決まれば次のセルの値も決まるように、引数が決まれば返り値がただ1つに定るという性質だ。

同じ入力であれば常に同じ値が出力される。参照透過性のない関数は、同じ入力でも返り値の値が同じとは限らない。

状態を変更する操作など副作用のある破壊的な操作の場合のときに参照透過性を保ちつつ、関数型で扱うには、クロージャを利用して状態のスナップショットを取るのである。

実はオブジェクト指向でも同様のことを行っている。下記はメソッドチェーン可能なように実装したカウンタの例だが、ここではカウンタの値をスナップショットとして持つオブジェクトが返り値として返っている。そのため、メソッドチェーンをしても正しくカウンタが動作するのだ。

このように関数型プログラミング的なことをみんなやっている。

続いて実行環境の話。関数は呼び出すたびに新しくスタックフレームをつくる。メモリは有限なので、当然、関数を呼べる回数にも上限がある。

コンソールで実行するとコールスタックが溢れ、オーバーフローが起こる。そこでES2015から”use strict”をつけて関数呼び出しを工夫する。

再帰呼び出しをするときに末尾呼び出しをするのである。こうするとインライン展開され、関数呼び出しではなくなる。この辺りはWebKitで試してほしい。

また関数呼び出しは遅いというイメージがあるが、10万個ぐらいのパーティクルのプログラムを書いてみたところ、loopで書くのもforeachで書くのもそう変わらなかった。

JavaScriptで特に関数型を使うと良いと思うのが、イベントハンドリングの処理。イベントは時系列データだからだ。ストリームからデータを取ってきて、何かを処理させたいというときは、関数型処理をする方が自然なのでお勧めだ。

Reactive Extensions(Rx)のプログラミングスタイルとは?

最後に登壇したのは、VOYAGE GROUPのTetsuharu OHZEKIさん。セッションタイトルは「FRP?なにそれ部材?」。

冒頭に「Rxの話だが、ファンクショナルプログラミングの話はしない。アプリケーションアーキテクチャの話もしない。ライブラリの話もしない。今日話すのはプログラミングスタイルについての話のみ」と宣言してセッションをスタート。

クライアントサイドにおけるData Mutation(データ突然変異)、Domain Eventをはじめ、File I/O、Network I/O、Network Statusなど、一方、サーバーサイドにおけるNodeステータスやServer monitoring、Logggingなど、図のようなものはイベントとして取り扱うことができる。

JavaScriptを書いていると、このようなプログラムを書いていることがあるのではないだろうか。

イベントは集合である。この例では、’bar’という集合の中からわざわざflagをジャグリングするようにプログラミングしている。

こういうときに使うのが、Reactive Extensions(Rx)である。複雑になったイベントをマネージして、自分のやりたいイベントだけを取り出すことができるようになる。

RxはC#を起源としている。マイクロソフトリサーチで行っていた「Volta」というプロジェクトで生まれた。

当時マイクロソフトに在籍していたErik Meijer氏および彼のチームが設計したのである。Meijer氏はHaskell98の策定にも関わった人物でもあり、関数型プログラミングやリアクティブ非同期プログラミングで有名なコンピュータ学者である。今は個人で会社を立ち上げている。

そして2009年、Reactive Extensions for .NETが登場した。このとき、「LINQ to Events」というキャッチフレーズが作れた。

イベントは時間単位で切り取ると、大きな集合のようなものになる。そこに対して、クエリーを投げC#のLinqで扱っていけるようにするというセールストークだった。

これは評価されたが、マイクロソフトテクノロジーがそんなに流行っているわけなく、さらに時代はモバイルに移っていく。

そんな中で忘れ去られたと思っていたが、2012年NetflixがJavaをベースにしたRxをオープンソースにしたのである。

Rxを構成する4つの要素をうまく使おう

Rxは基本的に次の4つの要素で構成されている。

Observable
Operator
Subject
Scheduler

この4つの概念をうまく使うことで、非同期をうまく乗りこなしているのである。

まずはObservableについて。Observableは表を見ればわかるとおり。値は向こうから飛んでくるが、値がまったく跳んでこないかもしれないし無限かもしれない。次のECMAScriptに入れたいという動きが、そのまま入るかどうかはわからない。

次にOperator。Observableに跳んできた値にOperatorを適用することで、Observableから流れてくる最大、無限個まである値に関数を適用して変えていくということができる。

巨大なシーケンスに対して関数を適用していった結果、新しい別の値を返す。mapやfilter、flatmapなどの関数がある。

続いてSubject。Observableは値を飛ばすことしかできない。それだと使う側が工夫しないといけないので、使いにくい。

例えば自分でイベントを起こしたい。そういう場合にObservableとして購読する(subscribe)ことは可能だが、実際に作った側が.nextという関数を呼び出して値を流し込んでいけるようにするのが、Subjectである。

最後はScheduler。ここではJavaScriptの話をする。Event Loopは図のようにくるくる回っている。

ある値がObservableに飛んできた場合に、その値をその場でsubscribeしているOperatorにどんどん値を飛ばしていくしか使い道はないように見えるが、そうではない。

例えばあるオブジェクトのコンストラクタやファクトリの中で何か値をsubscribeしたい場合、subscribeした瞬間に、Observerから値が飛んできて、その中でthisの初期化していないメンバーを触っていて、エラーを起こしたり、クラッシュしてしまうということがよくある。

そういうケースに対して、subscribeOnというOperatorと組み合わせて、Schedulerに次のイベントloopで取れるように書くと、実際の処理では、コンストラクタの生成が終わると値が取れるようにできる。つまり時間軸をモックできるのだ。

Rxのプログラミングのキーポイントは次のようになる。

最終的にユーザーが値を手に入れるまで、

イベントを送り出すObservableを見つける。
新しいチェックポイントとなるObservableを作って返して、変数に代入する。

そしてこれを繰り返す。

これにより非同期処理でも、スケジューラでタイミングをコントロールできるようになる。Operatorは小さな単位で作られているので簡単にまとめることができる。その例が次のプログラムだ。

次に最初に紹介したフラグだらけのプログラムは、Rxを使って書くと次のようになる。

このプログラムの途中のscanというoperatorでは状態を保持している。どのように実装されているかは、次のようになる。

trynextで分けている理由は、試行錯誤の結果。これはオブジェクト指向っぽく書いている。実装を見ればわかるとおり、bafooはイベントの発信点とすると、1つ一つの関数が中継地点となっている。

ここまでの説明でわかるとおり、RxはObseverパターンを拡張したものでしかない。つまりインターフェースを揃えて、呼び出し規約をメソッドチェーンの形でつなげていき、あらかじめライブラリ組み込みで、中間の集計オブジェクトをオペレータという形で提供する。

これがOperatorで、自分で自分を再定義して作っていける。IterableとObservableは対。今のところECMAScript6には入っていないが、ユーティリティクラスを追加するとこういうコードがかけるので、ぜひ、チャレンジしてほしい。

pullは非同期APIとして扱いにくい。というのも、イベントが来るまでブロッキングになるからだ。Erik Meijer氏もブロッキングがフリーになるなら、async/await構文なんていらないとつぶやいている。

そこでECMA262のプロポーザルでは、iteratorの結果をPromiseに変えるasync iteratorの議論が開始されている。

非同期のイベントを集めたい場合はPush型APIの方が使いやすいこともある。データベースからデータを取ってきたい場合はpull型を使った方がよいことがある。

ただ、push型とpull型は表と裏の関係なので、使い勝手はユースケースで変わる。プログラマとして何をやるのが一番よいのか、選んでいくしかない。本当に実用的なものはなんなのか、見極めていくことが大切だ。

関数型言語を学ぶことは、プログラミングスキルを高める上でも大事なことがわかった。これを機会に、学んでみてはいかがだろう。

今回の講演資料・動画

第12回勉強会「 Satis function( ){ … };」動画 ※荒井氏の講演は非公開
JavaScript の関数プログラミング要素について(清水智公さん)
Introduction to Rx without saying “reactive” or “functional”(Tetsuharu OHZEKIさん)

カテゴリー : デジタル・IT タグ :
CodeIQ MAGAZINEの記事一覧をみる ▶
  • 誤字を発見した方はこちらからご連絡ください。
  • ガジェット通信編集部への情報提供はこちらから
  • 記事内の筆者見解は明示のない限りガジェット通信を代表するものではありません。

TOP