関数型Scala(7):ラムダとその他のショートカット - Mario Gleichmann

この記事はMario Gleichmann氏による、「Functional Scala」シリーズの第7回「Functional Scala: Lambdas and other shortcuts | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年12月5日)




前回は、関数型プログラミングの世界における最も強力な概念のうちの1つ、すなわち高階関数を見てきました。本質的に、ある関数が高階と呼ばれるのは、他の関数を引数として受け取る場合か、結果として関数を出力する場合でした。この考え方により、抽象化の新しいやり方がもたらされるだけでなく、ある種の状態やいわゆるコンビネータ(関数ビルダと呼んでも構いません)を捕捉したり受け渡したりするといった、かなり便利なこともできるようになるのです。なお、コンビネータとは入力された関数もしくはその他の入力を元に新しい関数を構築するもののことです。


特に、新しい抽象化の形式に関して言えば、ユースケースに特化した変更されるロジックを関数から抜き出し、後には純粋な機能だけを残す方法について見てきました。特殊なロジックは独自の関数で定義され、引数として渡されます。このようにして、フィルタリング用の単一の関数と、それぞれでリストがどのようにフィルタリングされるかを決定する多くの述語関数が作られました。この種の抽象化が抽象的であるように思えるなら、もう一度フィルタ関数を掲載しましょう。

val filter = ( predicate :Int => Boolean, xs :List[Int] ) => { 
    for( x <- xs; if predicate( x ) ) yield x 
} 
…
val even = ( x :Int ) => x % 2 == 0 // ★1
val odd = ( x :Int ) => x % 2 == 1  // ★2val candidates = List( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 )
val evenValues = filter( even, candidates ) 
val oddValues = filter( odd, candidates ) 

これはもう知っています!1行目から始まる高階関数と、5行目(★1)6行目(★2)の述語関数が2つですね。これらの関数を背景として、リストと任意の述語関数をフィルタ関数に適用し、フィルタリングされたリストを受け取ることができます。そして、フィルタリングによって素数のリストを作りたいと思えば、適切な述語関数をもう1つ定義するだけです。さて、ここで疑問なのですが、関数フィルタに渡すためには、常に予め述語関数を定義しなければならないのでしょうか?何のための質問でしょうね?もちろん、フィルタに渡すためには関数が必要ですし、関数が天から降って来るわけではありません!もちろん、もちろんです。しかし、覚えていらっしゃるでしょうか。関数を扱った第2話で純粋な関数リテラルについて見ています:

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

あー、思い出しました?ここに書いたのは関数の中核です。つまり、引数のリストがあって、その後に関数矢印( => )と関数の本体が続きます。これは完全な関数で、ただ名前が与えられていないだけです。このようなやり方で関数を定義したら、後でその関数を参照する方法はなく、したがって後で呼び出すことはできません。名前のない、無名関数だからです。そしてここに、いわゆるラムダ式が登場します!これはまさに、純粋で無名の関数定義です。


これは私たちの疑問に対して、どう応えてくれるのでしょう?そうですね、無名の関数定義であっても、値を表します。これは他のあらゆる型のあらゆる値を定義し、それを名前と関連づけない場合と変わりません(後で参照するために、その値に別名をつけることをやらない、ということです):

val almostPi = 3.14159265                     // Double 型の値で、後に名前 almostPi を使って参照できる
2.71828182                                    // Double 型の別の値。今度は「無名」
val mult =  ( x :Int, y :Int  )  =>  x * y    // ( Int, Int ) => Int 型の値で、名前 multを使って参照できる
(  x :Int, y :Int )  =>  x + y                //  ( Int, Int ) => Int 型の別の値。今度は「無名」

Double 型の値を参照している別名を用いて関数を呼び出すことができるのと同様に( double( almostPi ) のように)、Double 型のリテラルを使ってその関数を呼び出すことも当然できます( double( 2.71828182 ) のように )。ふぁーあ。いつもの通りですね。その通りです。関数型プログラミングにおいて、ラムダ式を直接高階関数に渡すのは完全に普通のことなのです!そしてこれが、私たちのいくぶんわざとらしい質問に対する答えとなります。つまり、高階関数の呼び出しに使う前に、述語関数を名前と関連づけて導入する必要は必ずしもないということです。その代わり、関数を呼び出す際に都度定義することができるのです:

val evenValues = filter( ( x :Int ) => x % 2 == 0, candidates ) 
...
val positiveValues = filter( ( x :Int ) => x > 0, candidates ) 

OK、素晴らしい。しかし、一度しか使わない関数を前もって定義しなくてよくなるということ以外に、どういうメリットがあるのでしょうか?そうですね。儀式的なコードの量をさらに減らすことができるということがわかります。Scala型推論カニズム万歳!関数フィルタの型を細かく見ると、( Int => Boolean, List[Int] ) => List[Int] という型であることがわかります。そして、その型がわかるのはあなただけではありません。Scalaコンパイラも、関数の型を見抜くことができるのです!そして、コンパイラが述語関数の型を知っているので、引数の方を関数リテラルを使って明示的に註釈をつけなくてもよいのです(したがって、引数の型は、高階関数の呼び出しで関数定義をする際に、その都度決定されます)。

val evenValues = filter( x => x % 2 == 0, candidates ) 
... 
val positiveValues = filter( x => x > 0, candidates ) 

さらに、お望みであればもっと簡潔に書くこともできます。すべての引数を関数本体内で一度しか参照しないのであれば、関数リストの宣言を省略することができるのです!うーん、えっと、それでは、関数本体の中でどうやって引数を参照するのでしょうか?ここで、不思議なアンダースコア( _ )がはじめて機能するようになるのです。この記号は(後の回でも見るように)何でも屋です。今回は、与えられた引数リストを順番に参照するためのショートカットになります。関数本体で登場するアンダースコアは、与えられた引数の値と置き換えることができます。つまり、最初のアンダースコアは最初の引数の値を参照し、次のアンダースコアは2番目の引数の値を参照する、といった具合です。

val evenValues = filter( _ % 2 == 0, candidates ) 
...
val positiveValues = filter( _ > 0, candidates ) 

人によっては、この形式は簡潔すぎると感じるようです。趣味の問題だと思いますけどね。しかし、可読性の高いコードという観点からすると、この書き方はあまりに多くの情報を隠しているかもしれません(少なくとも、私のように静的型付け言語から来た人間からすると)。そこで、簡単なアドバイスですが、アンダースコアの記法を適用するのはきわめて「経済的な」場合だけとし、裏にある型がコンテキストから容易に把握できる状況に限定した方がよいでしょう。


実際にはアンダースコア記法は、正統的な関数定義において、コンパイラが与えられた引数の型を推論できる限りは使うことができます。コンパイルエラーが起きる例を示しましょう。

val mult = _ * _ // compile error : missing parameter type ...

次に示す、若干変更したバージョンはスムーズにコンパイルされます。関数本体においてアンダースコアで示されている各引数の型を明示的に記述できているからです:

val mult = ( _ :Int ) * ( _ :Int ) 

この場合、コンパイラには、関数の値を導き出すのに必要なものがすべて与えられています:アンダースコアは2回登場しますが、どちらもInt 型として註釈がつけられています。したがって、この関数の引数リストは2つの引数からできていて、どちらも Int 型となります。関数をこのかたちで定義するのがわかりやすいかどうかの判断は、あなたにお任せします。しかしながら、(少なくとも私には)もう少し読みやすいと思える妥協点があります。関数リテラルを別名と関連づける際に、式全体の型を明示的に宣言してもよいのです:

val mult : (Int, Int) => Int = _ * _ 

いいでしょう。ここでは、2つのアンダースコア両方のコンテキストは、関数の型註釈内で見事に記述されています。引数リストを見さえすれば、2つのアンダースコアの並びが混乱を招くことはありません。関数の本体が、例で示したように短い場合には特にそうです。

まとめ

今回は、関数リテラルの、ちょっとかっこいい新しい用語を学びました。それが、ラムダ式です。ここまで、その都度定義されて使い終わったら捨てられることになる関数を使うのが適切な状況をとりあげ、ラムダ式が役に立つところを見てきました。これにより、特殊な形式の関数につけられた奇妙な名前がレパートリーに加わったことになります。それに加え、関数の引数に対するプレースホルダとしてアンダースコアを用いることにより、儀式的なコードを減らす方法にも触れました。おわかりの通り、使い方は好みの問題かもしれません。こうした過程で型情報が失われてしまうかもしれないからです。しかしながら、コンパイラが型情報を見失うことは決してない、ということは本質的です。Scalaは静的型付け言語ですから(型情報を省略できる時もあるので、Scalaが動的型付け言語であるように見えるかもしれませんが)。そのような場合には、失われた型情報を私たちが提供しなければなりません。やり方は、関数本体でアンダースコアを書くたびに型情報を添えても、関数式全体に型を明示的に書いても構いません。いずれにしても、こうしたショートカットを活用する場合には、特に可読性という観点から見た結果について、明確に意識しなければなりません。