関数型Scala(2):関数 - Mario Gleichmann

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




関数型Scalaの第2話へようこそ!前回は、コアとなる考え方を調査し、式に基づく関数の適用を、関数型プログラミングの基本的な処理方式として抽出しました。今回は、Erik Meijer博士(※1)であれば、関数型プログラミングの必需品(the bread and butter)と呼ぶであろうものから始めましょう。それは・・・なんと・・・関数です(BGMとしてトランペットが鳴り響いていると想像して下さい)


関数を呼び出すためには(これは、関数を引数に適用すると表現されます)、まず、関数を定義しなければなりません。そこで、Scalaでは関数をどうやって定義するのか、その感覚をつかみましょう・・・

関数リテラル

前回のエピソードで示した、関数の定義を思い出しましょう:

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

したがって、関数を定義する上で行うべきなのは、関数が操作を行うパラメタ(と、Scalaが静的型付け言語であるため、その型)を宣言し、その結果がどのように算出されるかを、通常は与えられたパラメタを用いて定義することだけです。Scalaを使うと、関数をリテラル形式で定義することができます。これは、前述した関数の本質的な部品を定義することで行われます。つまり、パラメタのリストの後に関数の本体を定義します:

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

ここに示したのは、シンプルな関数リテラルです。まずパラメタ(どちらもInt型)のリストを宣言し、結果を算出する数式が後に続きます。両者は左辺のパラメタを右辺の関数本体と区別する=>(これは関数矢印(function arrow)と呼びましょう)によって分けられます。このリテラル形式は、特殊な関数の型(function type)のインスタンスを構築する糖衣構文(syntactic sugar)であることがわかります(後のエピソードで詳しく取り上げます)。


なんと!シンプルですね。しかし、必要な関数が、上述の例よりもいくらか「長い」、より複雑な処理を必要とするとしたらどうでしょう?もちろん、複数行にわたる関数は、関数の本体を波カッコで括ることで定義できます。

( a :Int, b :Int, c :Int ) => { 
    val aSquare = a * a 
    val bSquare = b * b 
    val cSquare = c * c 

    aSquare + bSquare == cSquare 
}

いくらか長くなったこの関数を見て下さい。これは、与えられた3つの整数が、いわゆるピタゴラスかどうかを判定するものです。待て待て、とあなたは言うかもしれません。いいでしょう。例の中には複数の行があります。言われていた波カッコも見てとれます。しかし、関数が呼び出し元に何を戻すかを記述した return 文がありません。それでは、関数の本体を一行ずつ見ていきましょう。


2行目から4行目にかけては、与えられたパラメタを用いて中間的な値を計算しています。この中間的な値を後から参照するために、val を使って別名を与えています。6行目では、もう少し面白くなります。ここにあるのは、前に計算された中間的な値を用いて作られた式です。Scalaでは、関数全体の結果は、常に関数本体の最後の式の値を評価したものになることがわかっています。そして、6行目にある最後の式は、当てはまるかどうかを示す(等価)関係を構築しているので、この最後の式と関数の結果は真か偽かになります。

関数の型

早合点しないでください。以前に、Scalaが静的型付け言語であると説明しましたし、そのために、関数のパラメタの型を宣言しなければなりませんでした。しかし、関数の結果となる値の型はどうするのでしょう?そして、ここで立ち止まらないでください。すでに言及した通り、関数リテラルから作り出されるものが、特殊な関数型のインスタンスであるなら、関数全体の型はどうなったのでしょう?いい着眼点です!


関数の結果となる値の型に関しては、それに答えるのに十分な知識をすでに得ています。これは単純に、関数本体の最後の式が評価された結果の値の型となります。それでは、関数全体の型がどうなるか、要約して少し考えてみましょう。関数には本質的な部品が2つがあります。それがパラメタのリストと関数本体であり、関数矢印(=>)で分けられています。さて、今見てきた通り、あらゆるパラメタには独自の型があり、関数の本体は本質的にはその結果によって表現されます。そして、その結果にも型があります。したがって、関数の性質は、関数に適用されるパラメタの性質(これが関数の型になります)と、関数の結果の性質(つまり、結果となる値の型)によって特徴づけられるのです。関数の型を表現する際にも、パラメタの型と結果の型を分ける必要があります。しかし、そうしたセパレータはすでにあるのではないでしょうか?なるほど、ここで、関数の型を表現できるに違いありません。


最初の例で言うと、1つめパラメタの値の型は Int で、2つめのパラメタの値の型も Int です。2つの Int の値を加算すると、Int の値がもう1つできますので、その結果も Int 型になります。これについては、関数全体の型を次のように表現できるのです・・・

( Int, Int ) => Int

一般的に、型とは、共通の特徴を共有する値の集まりと考えることができるのであり、上記の関数の型は Int 型の2つの引数を Int 型の結果にマップさせるようなあらゆる関数を表現しています。我々の書く関数は、そうした集まりに含まれる特定のメンバである一方、Int 型の引数を2つ取り、結果が Int であることによって関数の型を共有しているような、非常に多くの他の関数全体について考えることもできます(これは、同じInt型を共有している、非常に多くの整数値から、ある整数を選び出すことができるのとほぼ同じです)。


2つめの例についても、同じことが言えます。3つのパラメタは、すべて Int 型です。すでに見た通り、関数本体にある最後の式は評価されてbooleanの値になりますので、結果も Boolean 型になります。それでは、関数の型を推測してみてください・・・

( Int, Int, Int ) => Boolean

いいでしょう。これで、関数の型を推測し、表現できるようになりました。ここで、我々が関数定義を見ることによって型を推論できるようになったことはよいことですが、コンパイラについてはどうでしょう?コンパイラも同じことをします。つまり、与えられた関数の型を推論しようとするのです。コンパイラがわからなければ、時にはカツを入れて、関数の型を明示的に宣言しなければなりません・・・

ファーストクラスの値としての関数

全体として見ると、あなたがオブジェクト指向世界から来ているなら、関数はオブジェクトの中に置かれた普通のメソッド以外のなにものでもないように思われるでしょう。まずは、前回のエピソードを思い出してください。関数は与えられたパラメタにしか依存してはいけませんでした。それに対してメソッドは、オブジェクトのメンバである内部状態を参照することもできれば、さらに悪いことに、そのオブジェクトの内部状態を変更することもできます。こうした状態の変更は悪しき副作用と考えることができます(この側面については後のエピソードで立ち戻ります)。


さしあたって、メソッドと関数の違いをもう1つ別の角度からお見せしたいと思います。メソッドの型は何でしょう?うーむ。
メソッドが特定のオブジェクトのメンバにすぎないということはわかりました。オブジェクトそのものは、もちろん特定の型のインスタンスです。しかし、メソッド自体には型がありません。こう聞くと混乱してしまうようであれば(私自身は、最初にこれについて考えた時には混乱しました)、視点を変えてみましょう。すでに見てきた通り、あらゆる関数には型があります。そして、( IntBoolean )のような、他の型とと同じように、パラメタの型や他の関数内での結果の型として用いられた場合、何も特別なことはありません(最初のエピソードを思い出して下さい。そこでは畳み込みfold )の例を使って、関数を他の関数に引数として渡すという考え方に触れました。これについては、高階関数を調べる際に詳述します)。したがって、ある関数を他の関数に渡すことができる一方で、関数にメソッドを渡すことはできません。このことは、関数の結果となる値について考えると、より奇妙なことが起きます。つまり、他の関数を呼び出した結果として、ある関数を受け取ることは、ごく自然なのですが(へえ?自然?高階関数の説明を待って下さい)、関数の結果として、裸のメソッドが戻されると考えることはできないでしょう。メソッドは常に自分の属するオブジェクトを必要とするからです。


したがって、メソッドはそれ自身で独立した値と考えることができないのに対して、関数はそれができるのです。関数の市民権活動について何か聞いたことがあるかもしれません。この活動は関数が第一級の市民、もしくは第一級の値であると主張するものです。今ではその理由がわかるでしょう。関数は、今ある他の値や型と比べて、劣っていたり異なっていたりするものではないのです。そして、例えば Int 型の値に( val を使って)別名をつけ、その別名によってその値を参照できるように、関数に対しても同じことができるのです(なぜなら、関数もまた前述した関数の型を持つ値だからです)。

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

こうすることで、関数に対して add という「別」名がつけられます。この add を参照するときにはいつでも、等号の右辺にある関数リテラルによって定義された関数を参照しているのです。


前に述べたことを思い出して下さい。コンパイラは常に関数の型を推論するわけではなく、関数の型を明示的に宣言することで手助けしてあげなければならないのでした。この型推論は、再帰を活用する際に、通常、不安定になることがわかります:

val power = ( base: Int, exp :Int ) => if( exp <= 1 ) base else base * power( base, exp - 1 ) 

この関数をコンパイルしようとすると、コンパイラに関数の型を推論できないと叱られます。

error: recursive value power needs type

なるほど、コンパイラはこの関数 power を値として認識しました。当然、それでいいのです!しかし、コンパイラは私たちのちょっとしたなぞかけを解決するのに、何らかの助けを求めています。求めているものを与えてあげましょう・・・

val power : ( Int, Int ) => Int  = ( base: Int, exp :Int ) => if( exp <= 1 ) base else base * power( base, exp - 1 ) 

これが、関数についての完全な宣言です*1。すべてが俎上に上がりました。後でその関数を参照するための別名は、として導入されました。その値に対しては明示的に型を宣言したことで、コンパイラはおとなしくなりました。そして、パラメタのリストと関数本体からなる関数の値それ自体があります。これで完了です。Graham Hutton博士(※2)であれば、喜んで次のようにまとめるでしょう。

関数は、その関数に名前を与える等式、すなわち引数と、その引数によってどう結果が算出されるかを定める本体関数に名前を与える等式を用いて定義されます。

ところで、if ステートメントを見て下さい?おっと。私は、ステートメントについては、命令型の世界に由来すると説明しようしていないことはおわかりでしょう。実際、ここでは事実上 if 式になっています(これは、その関数本体での最後の式ですので、結果となる値を評価する式として扱われます)。そこで、何が違うんだと、あなたは尋ねるかもしれません。そうですね、式が評価されると常に、ある型の値となります。したがって、ifと呼ぶ以上は、あらゆる状況に置かれている値を評価しなければなりません。これはつまり、Scalaif を式として活用しようと思えば、else を省略することができないということを意味します(ただし、オブジェクト−関数型言語として、Scalaは両方の世界で使えなければならないので、Scalaif を式として使うのでなければ、else を省略できます)!

関数の適用

さて、最初の関数を定義したので、今度は使ってみましょう!この関数を、適切な型の具体的な引数の値に当てはめてみるのです!
Scalaにおいて関数を呼び出すのは、数学的な記法で定義された関数を適用するのと、実によく似ています。つまり、引数を丸カッコで括り、関数名の後に続けることで表示するのです。


なんとラッキーなのでしょう!関数にどうやって名前をつけるかをちょうど見てきたので(それには、関数リテラルを特定の関数型のとして宣言し、その関数を名前と関連づけます)、その名前を参照することで関数を呼び出せるようになりました:

val byteStates = power( 2, 8 )

確かに、実はあまり面白いことが起きていません。ここでは、関数 power の基数( base )に 2 を、指数( exponent )に 8 を適用し、10進数での1バイトを算出しようとしています。関数の適用が表現しているのは、もう1つの式でしかないことに注意して下さい。これは、評価の結果、ある型(その関数の結果となる値の型)のある値(その関数の結果となる値)となる式です。そして、もちろん、後々に参照するため、もう1つの別名とその値を紐づけることもできます(この場合には、byteStates)。そして、もうわかっている通り、この値の型を明示的に宣言する必要はありません。Scalaコンパイラが推論できるからです。つまり、この場合、値 byteStates は、単純に関数の結果の型となります(関数の型はすでにコンパイラに伝えられているからです)。


すべての引数は丸カッコに入っているので、結びつきに関して不確実なことはありません。関数の適用は、自然と、最も高い優先順位を付けられるのが常です。どの式が表現され、引数として使われるかは常に明確です。引数としての式?わかりました!この特徴はあまりにも自明であるように見えるので、次のことは通常触れる価値がありません。もちろん、関数は普通の値に適用されるだけでなく、より複雑な式にも適用されるのです。式が評価されると、最終的には特定の型の値になるので、コンパイラはその種の組み合わせを解決することもできます:

val isPythTriple_3_4_5 = add( power( 3, 2 ), power( 4, 2 ) ) == power( 5, 2 )

これはもう、頭痛のタネにはなりません。ここにあるのは、容易に分解できる、正しい式なのです。つまり、これは、2つのサブ式の間にある単なる等号にすぎません。右辺で行われているのは、「実にこの上なく複雑な*2」関数の適用です。( Int 型の2つの値を求める)関数 add の引数が、さらに2つのサブ式から現れるのであり、さらにこのサブ式は power(この評価結果はどちらも Int 型の値になる)に対するさらなる関数呼び出しであるとわかります。全体として、まだ説明されていない点はありません。

まとめ

なんという一日でしょう。これまで、Scalaが関数の型を文字通りにサポートしているのを見てきました。そして素晴しいことに、これについて、わからないことは何もありません。あとは、慣れる必要があるだけです。関数は、よく知られた一般的な型の他の値以上でもそれ以下でもありません。そして、他の型( IntBoolean あるいは List[String] のことを考えてください)を用いてできることは、関数を用いても行うことができます。これはすなわち、別名と関連づけて、受け渡し、関数呼び出しから戻されることなど、その他諸々全てです・・・


もちろん、話さなければならない欠点もいくつかあります。Scalaがオブジェクトー関数型言語であるため、いくらか妥協をしなければなりません。本当の変数を関数と共用したらどうなるでしょう?可変のオブジェクトを関数の引数として適用することはできるでしょうか?そうだとすると、関数の中でその値を変更し、ある種の副作用を引き起こしてもよいのでしょうか?ある関数が、すでに特定の関数型のインスタンスであるとすると、ポリモルフィックな関数とポリモルフィックなメソッドはどうなるのでしょうか(これについては、別のエピソードで扱います)?


ここでは表面をさらっただけです。ここで得ることができたのは、関数型の世界に一層踏み込んでいく上での優れた基礎です。最初に言った通り、ここではScalaにおける関数型プログラミングの必需品を習得しました。しかし、全部を語るとは言わないまでも、まだいくつか付随するものもあります。関数を定義するための、別の方法についても見ていきます。たとえば、別の関数や、特別な高階関数(もう言いましたっけ?)を活用する方法、あるいは既存の関数から別の関数を引き出すという新しい概念を思いつくなどといったことが挙げられます。しかし、こうしたものは、「関数型Scala」における、別のエピソードで扱われる題材です・・・




※1 Erik Meijer博士のことを単純に、関数型純粋主義者と呼ぶ人もいます。私にとっては、関数型プログラミングに私の興味を向けてくれた、本当に素晴しい人です。

※2 Graham Hutton博士は関数型プログラミングにおいて燦然と輝くもう1人の人物です。彼はProgramming in Haskellと呼ばれる素晴しい本を書きました。(しかし、注意して下さい。この本はHaskellの紹介を装っていますが、実は、世界支配とはいかないまでも、関数型プログラミングを拡大すべく努力しているのです)。

*1:訳註:無論、通常であれば関数の宣言は def を用いて行う。ここでは関数がであることを説明するためにこのように書いているが、プロダクションコードもこのように定義すべきであると推奨しているわけではないと考えられる。kmizushima様より指摘を頂きました。要約すれば、「def で定義されるのはあくまでメソッド。Scalaではメソッドが関数として使われることもあるが、ファーストクラスとしての関数は関数リテラルで記述するのが正しい」ということです。不用意でした。詳しくは、コメント欄を参照してください。(2011/03/22)ただ、def で定義したメソッドも関数の型で宣言された引数に渡すことはできます。この辺りが概念的に少し紛らわしいところです。(2011/03/23)

*2:訳註:大げさに表現しているが、もちろん冗談