関数型Scala(3):関数としてのオブジェクトとしての関数 - Mario Gleichmann
この記事はMario Gleichmann氏による、「Functional Scala」シリーズの第3回「Functional Scala: Functions as Objects as Functions | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年11月8日)
関数型Scalaの第3話へようこそ!
今回は、Scalaの内部を軽く見ていき、前回お話しした不可解な関数の型の謎を明らかにしていきたいと思います。つまり、今回は一般的な関数型プログラミングよりは、むしろScalaの専門的な話になります。
関数は、単純に関数リテラルを指定することで定義できるのだということを、思い出して下さい。この関数リテラルは、関数パラメタのリストと関数の本体で構成されています(両者は関数矢印 => によって分けられます)。さらに、特定の関数の型を持った名前つきの値としてその関数が宣言されるかもしれません:
val power : ( Int, Int ) => Int = ( base: Int, exp :Int ) => if( exp <= 1 ) base else base * power( base, exp - 1 )
それでは進めていきましょう!ここにあるのは、普通の関数で、Int 型の2つの引数に適用されることで、Int 型の別の値になります。したがって、関数の型は次のようになります。
( Int, Int ) => Int
目新しいものは何もありません!既にわかっている通り、あらゆる関数は特定の関数の型の1インスタンスなのです。
しかし、すべてはObjectなんでしょう?
おそらく、そう聞いたことがあるとおもいます。Scalaにおいては、すべてがObjectです。プリミティブもなければ、ラッパー型もなく、オートボクシングも(目に見えるところには)存在しません。しかし、それでは、このれっきとした関数はどうなるのでしょう?つまり、関数は関数型の世界に属し、オブジェクトはオブジェクト指向の世界に属しているのではないのでしょうか?確かに、Scalaはオブジェクトと関数のハイブリッドなので、両者の間で整合性をとらなければなりません。そして、Scalaにおいてはあらゆる値がObjectなので、関数もObjectなのです。わかりきったことじゃないですか!?
FunctionN
これまでに、Scalaで定義した関数はどれも、特定のFunction トレイトの特徴を持つ実装の1インスタンスになることがわかります(トレイトが何であるかということについて、手がかりを持っておらず、ただ、Javaについて聞いたことがあるというのであれば、トレイトについて、さしあたりインタフェースの強化版だと考えて下さい)。このFunction トレイトは、Function1 から Function22 まであります。なぜ、これほど多いのでしょうか?Scalaの製作者が重装備を思いつき、関数の重要性を示すために出費や努力を惜しまなかったということでしょうか?まあ、そんな感じです。Scalaにおいて、関数はObjectであり、そして、Scalaが静的型付け言語であるため、異なる数の引数を持つ関数それぞれに対して適切な型を提供しなければなりません。2つの引数を持つ関数を定義すれば、コンパイラはその型として Function2を選び出します。引数の7つある関数を定義すれば、Function7 になるわけです。そして、最大が Function22 であることから、引数を 23 以上取る関数を定義することはできません。残念ですね。そう思いませんか*1?
もう少し詳細に見ていきましょう。前回のエピソードで使った関数を思い出して下さい。これは、 Int 型の引数を3つ取り、結果が boolean の値となるもので、3つの引数が、いわゆるピタゴラス数になるかどうかを判定するものでした。
val pythTriple : ( Int, Int, Int ) => Boolean = ( a :Int, b :Int, c :Int ) => { val aSquare = a * a val bSquare = b * b val cSquare = c * c aSquare + bSquare == cSquare }
さて、どのFunction トレイトが使われるか当ててください。Function3 を選ばなかった方、残念ながらハズレです。
いいでしょう。3つの引数を取る関数であれば、Function3 になり、n 個の引数を取る関数であれば、FunctionN となります。それでまったく問題ないのですが、引数の方についてはどうでしょう?また、関数の結果の型についてはどうなるのでしょう?おそらくあなたは、型付きパラメタを思いついたでしょうし、それが正解です!トレイトのscala.Function3を見てみましょう。今回の話と関係のある部分だけを見せることにします:
trait Function3[-T1, -T2, -T3, +R] extends AnyRef { ... def apply( v1 :T1, v2 :T2, v3 :T3 ) :R ... }
先ほど書いた関数がどこにあるかわかりますか?3つの型パラメタ、T1 、T2 、そして T3 が引数の型となり、型パラメタ R が、関数の結果の型を表しているのです。さしあたって、型パラメタの前にある記号は気にしないで下さい。この記号が示しているのは、各パラメタの変位指定(variance)の種類です(引数が反変(contravariant)であるのに対し、結果は共変(covariant)としてふるまいます。しかし、心配しないでください。あなたがリスコフ置換原則と、スーパータイプの代わりにサブタイプをどう使うかということについて知っているならば、型パラメタの変位指定が何を表現しようとしているのかについて、適切な直観を得ることができるでしょう)。
さて、我々のよく知っている関数リテラルによって定義される具体的な関数は、どれも、コンパイラが適切なFunction トレイトのインスタンスに変換します。そのFunction トレイトでは、型パラメタが引数の型と関数の結果の型を用いてパラメタ化されます。ちょっと待ってください。我々の書いた関数には、すでに型がありますよね。上記の例を見てください。関数 pythTriple の型は明らかに次に示すものとなります:
( Int, Int, Int ) => Boolean
そうです!しかし、これは実際に操作される適切な関数の型のための糖衣構文にすぎず、次に示すものと変わらないのです:
Function3[Int, Int, Int, Boolean]
さらに、お気づきの通り、ある形式の型宣言は、別の形式の糖衣構文にすぎず、両者は交換可能なのです。したがって、我々の書いた関数は次のように宣言することもできたのです:
val pythTriple : Function3[Int,Int,Int,Boolean] = ( a :Int, b :Int, c :Int ) => ...
よろしい!しかし、問題が1つ残っています。それでは、抽象メソッドである apply は何を表現しているのでしょうか?そうです、このメソッドが、関数を適用する際に呼び出されるものなのです(したがって、関数の本体はこのメソッドの中に位置づけられます)!すでに別の種類の糖衣構文をかぎつけましたね?正解です。これまでに示した、わかりやすく数学っぽい関数の適用を表す記法は、単純に引数を丸カッコで括り、関数名の後に続けるというものでしたが、これは、与えられた引数でメソッド apply を呼び出す糖衣構文に他なりません。これら2種類の記法は、もっとおかしな混ぜ合わせができます。
一方で、与えられた関数の applyメソッドを明示的に呼び出すことで、関数を適用することもできます:
val isPythTriple_3_4_5 = pythTriple.apply( 3, 4, 5 )
逆に、適切なFunction トレイトを実装し、必要な apply メソッドを関数本体として定義することで、関数をつくることもできます。今回の例では、Function3 を使います:
val isPythTripple2 : (Int,Int,Int) => Boolean = new Function3[Int,Int,Int,Boolean]{ def apply( a :Int, b :Int, c :Int ) :Boolean = { val aSquare = a * a val bSquare = b * b val cSquare = c * c aSquare + bSquare == cSquare } }
誘惑に注意
関数を定義するのに、よりオブジェクト指向的なスタイルを用いて、上記の例で示したように FunctionN に対する無名の実装を提供したり、特定の Function トレイトを継承した新しいクラスを作ることさえもできます(その場合、関数を構成するのはそのクラスのインスタンスであって、クラス自体ではありません)。そこで、ふるまいだけでなく、状態も導入したいという誘惑にかられます。クラスであれば、メソッドだけでなく、フィールドも宣言できるからです。そして、そうしたフィールドは変数としても宣言できるので(覚えていますか。Scalaは関数型の世界と命令型の世界の両方に貢献しなければならないのです)、副作用のある関数(つまり、引数だけに依存するのでない関数、あるいは適用の結果として単一の値だけを生成するのでない関数です)を書こうとした時に妨げるものはありません:
class Last( init : Int ) extends Function1[Int,Int]{ var last = init def apply( x :Int ) : Int = { val retval = last last = x retval } } val last : Int => Int = new Last( 1 ) val x = last( 5 ) // x == 1 // ★1 val y = last( 5 ) // y == 5 // ★2 val z = last( 8 ) // z == 5 ... val a = last( 5 ) // a == 8 // ★3
よろしい、これ自体はそれほど役に立つ関数ではありません。しかし、これによって、Scalaが関数型のスタイルを促進しているかどうかという問いに対しては、いくつかの弱点が示されています。少なくともScalaでは、純粋ではない、副作用のある関数を書くことを防げません。思い出していただきたいのですが、純粋な関数は引数にのみ依存し、ある引数 x に対していつ適用させても、同じ結果 f(x) になります。これはその引数に適用させた回数にも依存しません。(この性質は、関数 last では明らかに破られています。実際に、13 行目と14 行目(★1と★2)を慎重に見れば、関数が同じ引数 5 に対して2回適用されているのがわかると思いますが、結果は異なる値になっています)。この性質は、「関数の等価性(the Equality of Functions)」と呼ばれることもあります。引数が等価であれば、関数を適用しても等価のままだからです。あなたが、この性質に対する、もっと正式で数学的な説明が欲しいと思うのであれば、その好奇心を満足させることができます。 この性質は次のように説明できます:
x == y <==> f( x ) == f( y )
確かに、これは数学の講義でありません。しかし、あなたがもう少し読み続けてくれるなら、純粋な関数の持つ興味深い性質をご紹介します。前述した通り、同じ引数に対して関数を適用すれば、常に同じ値が返されます。そして、これは、関数をいつ呼び出しても同じ値が返されるということを意味します。したがって、関数型の世界には時間の概念がないのです。なるほど、こうして輪が完結しました。時間の概念がないのであれば、関数の呼び出しをどういう順序で行うかは問題でなくなります。つまりどういう順序で実行されてもよいのです。このことは、第1話で、関数型プログラミングを命令型プログラミングのパラダイムと比較した際の、興味深い特徴でした。命令型の、状態を変化させる世界では、ステートメントがどの順序で実行されるかということを常に意識しなければなりません。そして、前述の例を見直すと、ここで書いた関数は純粋ではないので、再び時間が関係してきてしまっています(17 行目(★3)を見て下さい。結果となる値について説明するには、もはや関数の定義を見るだけでは十分でなく、関数が呼び出される順序についても意識しなければなりません。この場合であれば、どの値が関数に対して最後に適用されたのかが問題になります)。
ポイント
今回のエピソードで覚えておかなければいけないものが1つだけあるとすれば、関数に状態を導入するのは危険な誘惑だということです。関数リテラルのスタイルとその根底にあるオブジェクト指向の記法を望みのかたちで自由に混ぜ合わせることができても、です。関数が状態を持ってしまうと、とたんに、実行時に時間の影響を受けることになります。関数を定義する際にリテラル形式にこだわれば、そのリスクを最小限におさえることができます。このやり方では、状態を導入するのが一層難しくなるからです。ただ覚えておいて頂きたいのは、少なくともScalaにおいては、関数は常に特定の Function 型のインスタンス(つまりオブジェクト)にまで煮詰められるということです。そこから逃れる術はありません!
付録
Scalaでは、関数が Object まで煮つめられるので、Javaでも似たような解決策があるかもしれないと思ったかもしれません。そして、実際に、Javaで少しでも関数型プログラミングを行うことを目論んだライブラリもあります。lambdaj や、Functional Java を見たことがあるかもしれませんね。私自身も functJ というライブラリを昔書いたことがあります。これは、関数定義を行う際に、どこまで定型コードを避けられるかを実験したものでした。次回以降のエピソードで見ていくように、Scalaによって、糖衣構文としての関数リテラルがスムーズに統合されているだけではなく、他にも関数型プログラミングの考え方がいくつかJavaプラットフォームにもたらされています。
*1:訳註:もちろん、冗談