関数型Scala(1):導入 - Mario Gleichmann

この記事はMario Gleichmann氏のブログエントリ「Functional Scala: Introduction | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年10月28日)


また、当記事の翻訳許可を求めてメールした際、その返信として氏から暖かいメッセージを頂きました。ここに紹介させて頂きます。Danke schön, Mario!!

first of all let me say that all our prayers and thoughts are with you and all japanese people! For all of us here, the tragedy's so unbelievable - we just feel for all of your sorrow and pain and hope that our little donation will bring at least a bittie help!


まず、私たちの祈りと思いがあなたがた日本人の皆さん全員と共にあるということをお伝えさせて下さい。ここドイツに住む私たちにとって、今回の悲劇は実に信じられないものです。私たちはあなたがたの悲しみと痛みを感じ、私たちのわずかな寄付が少しでも助けになることを祈ります。

なお、この翻訳について「電子書籍だったら買ってもいい」と思って頂けるようであれば、その範囲の金額を、震災の義援金としてどこかお好きな団体に寄付して頂ければ幸いです。




「関数型Scala」シリーズの第1話にようこそ。Scalaの位置づけは、いわゆるオブジェクトー関数型言語ですが、Scalaに関する議論と記事のほとんどは、これまでオブジェクト指向的な特徴に中心が置かれていました。これを読んでいるならば、あなたは、Scalaのより関数型の側面について学びたいと思っているのではないでしょうか。そうだとすると、あなたは正しい場所に来ました。


これから始まるエピソードについての構想は、私が「関数型Scala」について行ったいくつかのプレゼンテーションに基づいています。私がこのテーマについて書き、また話すことを決めたのは、一般的な関数型プログラミングと、特にScalaに関する知識を確固たるものにしたかったからであり、また、Scalaを学びはじめた方に、私の考える関数型としての特徴を学ぶ上での手助けができると考えたからでもあります。


特に、Scala関数型言語としての特徴を正しく備えているか、ということについては、重要な議論が過去にいくつかありました。これについて判断するには、まず、Haskellのような広く認められている関数型言語でよくある、中核的な考え方について明確にしなければなりません。そうした考え方が、Scalaでも提供されているかどうか、提供されていたらどんなかたちであるかを見ていき、それを限界まで押し進めたいと思います。では、前置きはこのくらいにして、Scalaにおける関数型プログラミング(FP:Functional Programming)の世界に入っていきましょう。

関数型プログラミング」対「命令型プログラミング」

だれかが関数型プログラミングのメリットについて話しているのを、これまでに聞いたことがあるならば、関数型プログラミングの素晴しい約束が、本当にこのアプローチに従えば達成できるのかということについて、あなたは疑問に思ったに違いありません。ここで約束されるのは、(我々全員の探し求めている)単純さ、明確さとエレガントさといったものなのです。Liftと呼ばれているWebフレームワークの作成者であるDavid Pollakの言葉を少し引用します。どうやら彼は、最初は命令型プログラミングのパラダイムに学びましたが、関数型の世界に入った後は、関数型プログラマとしての自信を持つに至ったようです。

しかし、最も重要なのは、Scalaが私に、異なったやり方でプログラミングし、プログラムについて考えることを教えてくれたということです。バッファの割り当て、構造体やオブジェクト、さらにそうしたメモリの変更という観点から考えるのを止めました。その代わりに私は学んだのは、プログラミングのほとんどを入力を出力に変換することだと考えることです。このように考え方を変えることで、欠陥率は低くなり、コードはよりモジュール化し、テストしやすくなりました。


どうしてこれが可能なのでしょうか?我々は、オブジェクト指向(命令型言語の典型例としてのオブジェクト指向言語を用いるもの)を用いさえすれば、モジュール化され、保守しやすいコードが書けると教わったのではないでしょうか?関数型プログラミングを命令型プログラミングと区別する決定的な特徴は何でしょうか?


おそらく、こうした本質的な特徴を見分けるのに、一連の整数を合計するといったシンプルなタスクを解決する実装を比較することができるでしょう。1から10でやってみましょう。 まずは命令型スタイルで行い、次に関数型スタイルを用います。Scalaはオブジェクトー関数型言語なので、両方の解決策を示すのに使うことができます。

命令型スタイル
var sum = 0
var i = 1
while( i <= 10 ) {
    sum = sum + i
    i = i + 1
}

このコードを詳しく見ても、特筆すべきものは何もないでしょう。その理由の大半は、私が、あなたの注意を特定の題材に向けていなかったことにあります。それに加えて、あなたが命令型言語を用いてコードを書くのに慣れているならば、上記のコードに何も変わった点はありません。変数が2つあり、それぞれループを実行している間に、新しい値が割り当てられます。ループを抜けた後は、変数 sum が最終結果を保持することになります。普通ですよね?
変数に対して(中間的な)値を適切に割り当てることは、ステートメントを適切に並べることで保証されます。このステートメントは、値を計算し、値の割り当てを正しい順序で行うよう指示するようなものです。上記のコードで、4行目と5行目を入れ替えたら、望む結果にはならないでしょう。


本質的には、このシンプルな例を抽象化し、次のように述べることができます:

命令型プログラミングとは、プログラムの状態を変えるステートメントを正しく並べることで処理を記述するプログラミングパラダイムです。


変数の状態を変えられるという事実から、もう少し興味深い結論が導き出されます:2つの変数をリセットせずに、複数回ループを呼び出したらどうなるか想像して下さい。もしそうだとすると、変数 sum が値1から10までの合計だけを保持していると期待することができなくなります(sum の値はループが何度呼び出されたかに依存します。そうですよね?)。このコードの断片の持つ意図を理解するためには、一連のステートメントと変数への割り当てをすべて追いかけなければなりません。ある一行の意味を理解しようと思えば、その周りにあるステートメントの意味も理解しなければなりません。これは、変数への再割り当てのフローを追う必要があるからです。そして、変数の割り当てのフローを適切に追いかけるためには、ステートメントが実行される順序に依存する必要があります。


つまり、命令型プログラミングについて考える際には、心に留めておかなければならない主要な特徴が2つあります。命令型プログラミング(Imperative Programming)は、命令(instruction)に基づくスタイルだということを意味しており、これは処理がどのように行われるべきかということを正確に定義する単一の命令が連続することで表現されます。この命令に基づくスタイルでは、主に変数の(再)割り当て(と、そこからのあらゆる帰結)という処理手法によって指示が出されます。

関数型スタイル

関数型スタイルで書かれる解決策を見てみましょう:

val sum = fold(  1 to 10,  _ + _ )

なんと、一行だけです!しかし、ズルをしていませんか?ループを抽象化して、fold と呼ばれるメソッド(か何か)に入れただけではないのでしょうか?ここで、fold について深く見ていくこともできますが、しかし、今のところは、変数の(再)割り当てに基づくループは fold の内部にはないのだと、私を信じて下さい(後の章で fold については深く見ていくことにします!)。それでも、このコードの中に変数の(再)割り当てがないことを示す、間接的な「証拠」はあります:

val vs. var

まず第一に、変数 sum の宣言を見て下さい。ここでは、キーワード val を用いて宣言されています。val によって、sum は再割り当てのできない不変な値として定義されます。別の値を sum に割り当てようとすると、コンパイラに怒られることになるでしょう(だからこそ、我々は命令型の例で sum と i を定義するのにキーワード var を使わなければならなかったのです)。関数型の例では、sum のことをいつでも式の右辺として使用できるエイリアス(もしくは、その式を評価した値)と考えても構いません。


Scalaがオブジェクトと関数型のハイブリッドであるので、どちらの世界でも使えなければならないのです。しかし、もし関数型の側を旅行したいのであれば、値しか使ってはいけません。(階段本*1にあるように、関数型プログラマは val だけを使うべきで、var の使用は冒涜と考えられます)。

関数

わかったこととしては、fold は関数であり、2つのパラメタを取るということです:それが畳み込む(fold)値の集合と、その値をどう畳み込むかを伝える別の関数(ですよね?)です。これはつまり、値を畳み込み、合計の代わりにその結果を受け取りたいと思ったら、掛け算を行う別の関数を投げ込めばいいということでしょうか?そう、その通りです!この単純な例により、関数型プログラミングにおける中核的な特徴が示されています(ただし、関数型プログラミングの唯一の特徴ではありません):これは、関数を引数に適用することで、望む値(例えば、整数の合計)を受け取ろうというものです。関数とその特徴について深く掘り下げるまでは、関数のことを数学的な意味でとらえておくことができます:

関数とは、1つ以上の引数をとり、1つの結果を生成するマッピングなのです。(このマッピングは、引数からどのように結果が計算されるかを定める関数の定義によって行われます。)

ここにあるのは、関数のためのきわめて短い定義です。しかし、この比較的単純な定義により、実に強力な処理方式が得られます:あなたは、任意の結果を得るのに、適切な引数として関数を適用するだけでいいのです。これらの引数が他の関数である場合もあるので、新しくて強力な抽象概念を導き出すことができます。(これは、いわゆる高階関数(Higher Order Functions)を経験するときに見ていきましょう)。今や、処理に関する中核的な考え方は、一連の命令(その処理がどのように行われるかを正確に定義するもの)から、関数と値に基づく式の記法へ移りました。もはや、処理を理解するために、命令の計画を追いかける必要はありませんただ、こうした式を単一の要素に分解すればよいのです。


上記の定義では、きわめて重要な単語を1つ見落としているかもしれません:関数の結果は、その引数だけに依存するのです。グローバル変数がなければ、副作用もありません。これは、数学的な感覚で関数を見るときにも、あなたが予想もすると思われるものです。単純な関数定義を見てみましょう:

val add = ( x: Int, y: Int ) => x + y 


この関数定義では、所与の(正式な)パラメタ x と y に対して、どのように結果を算出するかを記述しています。これら2つのパラメタのことを、変数と呼ぶこともできます。しかし、数学においては、これらの変数は常に固定された値であり、何らかその時の値(つまり、関数適用時の実際のパラメタ)に適用されて関数に渡されるのです。同じことは、関数型言語において定義された関数にも言えます ー もう少しわかりやすく言うと次のようになります:関数が適用される値は、単なる値です。それ以上でもなければ、それ以下でもありません。しかし、値であるがゆえに不変なのです(命令型言語においては、変数はメモリ内の特定の領域を参照し、そこに値が入っています)。

ステートレス vs. ステートフル

ここまで、変数という概念が有効なのは、命令型言語の範囲内でのみだということを見てきました。命令型言語では、変数の値は複数回変更されるかもしれません。(数学のような)関数型の世界にあるのは、関数と値だけです。変数名は、変えることのできない特定の式に対する別名以外のなにものでもありません。その式を参照する際にいつでも使用できる別名にすぎないのです。お望みであれば、こうも言えます。あるのは、定数と値とパラメタだけなのです。


それゆえに、関数型言語には割り当て操作という考え方もありません。そして、値の(再)割り当てのようなものがないため、純粋な関数型プログラムには状態というものが存在できません。なんと、状態がない?しかし、状態はなくてはならないのでは?ステートフルセッションBeansがあれば、ステートパターンもあります。状態という考え方がないとすると、どうすれば、カウンタのようなものを実装することができるのでしょうか?後でわかることですが、関数な世界には、状態がない(それゆえ副作用もない)ことを扱うために他のメカニズムが存在します。さらに、関数型スタイルにおいてカウントを処理することも(これから見ていくように)できるのです。


我々がそれらの重大な違いを理解し、命令的な世界と機能的な世界についての若干の重要な考えに名をつけた今、我々は関数型プログラミングについて最終的に特徴を描写する準備が整いました:

関数型プログラミングというプログラミングパラダイムでは、処理を数学的関数の評価として記述し、状態や共通のデータ、それゆえ副作用も避けるのです。

恐れることはありません

恐れることはありません。こうした考え方については、より詳細に再度取り上げますし、Scalaにおいてどう現れているかについても見ていきます。この最初のエピソードでは、一般的な背景を紹介し、関数型プログラミングという考え方にあなたの気持ちを向けようとしたのです。これから、Scalaを使うことでどこまで行けるのかを見ていきます。


あなたが、Scala関数型プログラミングに不慣れであっても、恐れることはまったくありません。次のエピソードは、ゼロから始めます。その際、あなたの武器のバリエーションを広げるような、新しい考え方と問題解決のための概念を提示するよう試みます。


「関数型Scala」を最終的に理解するまでに、私はおそらく2回は習得に失敗しています。私にとっては、あまりに奇妙に見えて、理解できなかったからです。しかし、一度コツをつかんで、最初のハードルを乗り越えれば、実にスムーズに事は進みました。言いたいのは:Scala関数型プログラミングをする上でもきわめて優れており、あなたがプログラミングに興味を持っているならば、たとえそれが最初は奇妙に見えても、実際に学ばなければならないということです。「関数型Scala」を学ぶことは、はじめてプログラムを学ぶことに似ています。これは楽しいですよ!


そのためには、考え方を変えなければなりません。私に、ついてきて下さい。




プログラミングScala

プログラミングScala

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

*1:訳註:表紙に階段の写真が載せられた、『Programming in Scala: A Comprehensive Step-by-step Guide』のこと。我々で言う『コップ本』。