前ページ | トップページ | 次ページ

4階:関数

基本

def myFunc(x: String, y: String*): Seq[String] = {
	for (z <- y; if z containsSlice x) yield z
}

println(myFunc("be",
	"to be or not to be",
	"You could understand it if you were a good old man",
	"Hey! That's a bear!",
	"bees are flying around the garden flowers.",
	"And your bird can sing"
))

関数とは要するに引数付き遅延評価式なので、「def」を使って定義する。引数で、「*」付き型名は、それが可変長引数(Seq型)であることを示す;その実引数がSeq型の子なら、「: _*」を付けて渡せば、その実引数は「Seq[実引数の要素型]」へと変換される。引数の丸括弧の後ろの型が関数の返値の型で、その定義式から明白な場合は省略して良い(定義式内で自身を呼び出す関数、すなわち再帰関数の場合は省略は不可。なお、定義式末尾で自身を呼ぶ末尾再帰は、Scalaでは最適化される(すなわち、呼び手のスタックフレームを流用する))。また、返値を持たない手続きは厳密には「(引数): Unit =」だが、この場合は簡略表現としてさらに「=」も省略できる(「(引数) 定義式」の形)。引数が無い時は丸括弧も省略できるが、このやり方はテンプレート内ではゲッタ・セッタ絡みの問題を引き起こすので、あまりお勧めできない(オブジェクト指向解説ページで詳説)。既に述べたように、関数定義式ブロック内でさらに関数やテンプレートを定義可能である。return式(scala.Nothing型)はJavaと同様だが、ブロック式=定義式の時の、そのブロック式の最後には(そのブロック式の値が返値として使えるために)不要である。また無名関数内(と関数・メソッド内以外のところ)ではreturn式は使えず、また、無名関数が呼び出す関数のreturn式が評価される前にその無名関数を投げたメソッドが終了していた場合はscala.runtime.NonLocalReturnExceptionが投げられる(無名関数の実体はそのメソッド内に在るため)。

関数自体の型は「(引数の型) => 返値の型」で表す;引数が一つだけなら丸括弧は省略して良い。上なら「(String, String*) => Seq[String]」ということになる。


以上の引数はいずれも事前評価渡し、すなわちその引数名に束縛される前に実引数が評価されるわけだが(すべてがオブジェクトであるScalaでは値渡し・参照渡しという区分は無意味であるので、事前評価渡しと値渡しを混同すべきではない)、「:」と型名の間に「=>」を入れることで、遅延評価渡し、すなわち無評価で実引数を渡すこともできる;これを使えば「式」自体を丸ごと渡せ、実際もっぱらそのためにこそこの遅延評価渡しは随所で使われている。

def until(f: => Boolean)(p: => Unit) {
	while (!f) p
}

var i = 0

until (i > 10) {
	print(i + " ")
	i += 1
}
println

関数定義で丸括弧が二組なのはすなわち「カリー化」なのだが、これを説明するには高階関数に対する知見が必要なので後述とする(ここでは要するに、そうでないと「引数の数が合わない」エラーになるのだ、とここでは理解しておけば良い)。引数が一つなら、それに渡す実引数を指定するのに、Scalaでは丸括弧だけでなく波括弧も使える(もちろんブロック式であり、つまりは事前評価渡しの場合はそのブロック式の値が引数の型に適合していなければならない);カリー化によって関数は引数ごとに別々の括弧で対応する実引数を受け取れるようになるので、それで上のような「疑似構文」(と言うか、Scala的には「疑似構(造)式」)が関数で簡単に定義できる、というわけである。

高階関数

Scalaでは、すべてはオブジェクトであることは既に何度も書いているわけだが(ちなみにそんなわけで、リテラルにも直接その型のメソッドを適用可能である)、無論関数も例外では無い。つまり、関数の引数に他の関数自体を渡せる;関数の値を、では無く、関数の定義式自体を、である(無論その逆も可、すなわち、関数の返値として関数そのものも返せる。ちなみにこのような、関数を引数または/かつ返値とする関数を高階関数と呼ぶ;値を求めるのに二段階以上のプロセスを経るからだ)。やり方は非常に簡単で、引数の型を上記した関数の型にし、実引数には関数名だけを渡せば良い。これらは、JavaScriptを知っている人ならむしろまったく自明のことだが、しかし型に厳密なScalaでは次のことに注意されたい:ある関数型に対し、返値型は普通にその子型も可だが、引数型の方は逆に、その型かまたは「より祖な」型でなければならない。

def func1(f: String => String) {println(f("Huh!"))}
def func2(o: AnyRef) = o.toString
func1(func2)

AnyRefは、既に触れたようにJavaのObject型に等しく、従って当然String型の祖型である。上では引数がそのAnyRef型の関数を、引数がStringであることを要求する関数型引数へ渡しているわけだが、しかしそうして渡された関数の評価時には、AnyRefの引数にStringの実引数が渡されることで、結局実行時の親子関係が「逆の逆」となってちゃんと満される。もしこれが逆に、関数型引数の引数をAnyRefとし、それへと渡される関数の引数をStringとするならば、評価時には「Stringを要求する引数にその祖型であるAnyRefを実引数として渡す」ことになってしまい、Stringよりも情報の少ないAnyRefをStringとして扱うことによる「不定情報へのアクセス」を引き起こすことになる(だからオブジェクト指向では、子型を親型とも見なせるのみで、その逆は許されていないわけだ)。このように、「より祖な型のみ受け入れ可」であることを「contravariant」(反変:数学の圏論に由来する、プログラミング的には変な用語で、単純に「祖型渡し」と呼べば良いように思うが、どうやら最初の用語で定着しつつある模様)と言い、逆の、つまりは普通の「より子な型のみ受け入れ可」であることを「covariant」(共変)と言う(まだまだ日本語資料が弱いScalaでは、英語文献に当たる時に頻繁にそれらの語を目にすることになるので書いておく)。もっとも、反変は普通は関数型の引数の型でしか使われないので、「普通共変」とだけ覚えていれば良いだろうと思う。

なお、この関数型を示す「=>」は右から評価されるので、遅延評価渡しの場合は「(=> 引数型) => 返値型」と、引数が一つの時も括弧で囲まねばならない(さもないと、「=> (引数型 => 返値型)」すなわちただの「引数型 => 返値型」だと見なされる)。

無名関数

上で、次のようにできればもっと便利ではないだろうか。

func1(o: AnyRef => o.toString)

これならfunc2をわざわざ定義する必要が無くなる;実際これは可能で、このような関数を無名関数と呼ぶ(Java的には、要するに無名クラスの関数版である)。引数の型指定(返値は常に型指定無し)は、func1の引数型から明らかに推定可能なら(要するに同じ型なら)省略して良い(上では逆に、明らかに違う引数型なので明示している;複数ある引数型の一つでも違う場合は、すべての引数の型を明示しなければならない)。

無名関数には便利な(しかしちょっと分かりづらく、おそらくに可読性を下げる)簡略表記があって、「_」が、他の意味に取れない場合に限り、それを含む式を定義とする無名関数を表すものとして使われて良い。

_ + 1		// x => x + 1
_ * _		// (x1, x2) => x1 * x2
(_ : Int) *2	// (x: Int) => (x: Int) *2
_.map(_ + 1)	// x => x.map(y => y + 1)

また、メソッドまたは遅延評価引数を結果とする無名関数を次のように表記できる。

Math.sin _	// x => Math.sin(x)

さらにまた、「x => x match {case パターン => ブロック}」(無名関数によるパターンマッチ)で、「x => x match」の部分は省略可能である。

カリー化

def func(x: Int, y: Int, z: Int): Int = {}

このような関数は、「(x, y) => (func(z) => Int)」へと変換できる:つまり、xとyが既に得られている状態では、上の関数は「残るzを得さえすれば結果を出せる関数」へと変換できる。さらに「xが得られれば、後はyを得れば、『残るzさえ得れば結果を出せる関数』を返す関数」も可能であることを考えるなら、結局「x => (y => (z => Int))」とすることができる;このようにして、複数の引数の関数を、引数一つの関数の連鎖へと変換することを「カリー化」という(ちなみに、この例では分かり易くするために括弧で囲んだが、「=>」の評価順は右からなのだから、実際には括弧は不要である)。そのようなカリー化関数は、Scalaでは次のように定義する(なお、Scalaの場合は必ずしも引数一つずつにしなければならないわけでは無い;引数二つの括弧と一つの括弧、あるいはその逆等、好きなように分けて良い)。

def func(x: Int)(y: Int)(z: Int): Int = {}

この時、最初の「残るzを得さえすれば結果を出せる関数」は「func(x)(y) _」、また「後はyを得れば、『残るzさえ得れば結果を出せる関数』を返す関数」は「func(x) _」とScalaでは表される(「_」無しだと「引数が足りない」エラーになる。ちなみにfunc自体を呼ぶ時は「func(1)(2)(3)」のように定義の括弧に応じた括弧を各々与えねばならず、「func(1, 2, 3)」だとエラーになるので注意)。こんなことをして何が嬉しいのかというと、

def plus(x: Int)(y: Int): Int = x + y
def plus1(y: Int): Int = plus(1) _
def plus2(y: Int): Int = plus(2) _

のように、ロジックは同じで定数だけが異なるような関数、いわゆる派生関数を、ロジック本体には手を入れずに作れるからである。この「定数」を「処理に利用する関数」にまで拡張すれば、用途に合わせて万能ロボットの腕パーツだけ取り替えるような、そんな関数を恐れずかつ本質的ロジックにのみ集中して書けるようになる;要するに、プラグイン可能関数、というわけだ(これはそのまま昨今有名な「フレームワーク」や「プラットフォーム」そのものを作るのと根本的には同じ発想である;コンポーネント化可能な関数なわけである)。

暗黙引数

引数丸括弧群(一つだけをも含む)の最後に一つだけ、「implicit」で修飾された引数リストを囲む丸括弧が可能である。これは「暗黙引数」で、それに相当する実引数が省略された場合、第三の名前空間である暗黙名空間から該当するものを捜し出して暗黙に補う。

暗黙名空間は、捜索されるものの型名をキーに、implicit修飾されたメンバあるいは引数が示す単純型名(つまり、「.」を含まない名前)でアクセス可能なメンバ名、及びその捜索されるものの型名と何らかの形で関係するシングルトンに含まれる同じくimplicit修飾されたメンバが示す単純型名でアクセス可能なメンバ名より成る。ただし、型名空間に属する名前及びトップレベルのシングルトン名は除く(と言うか、それらはそもそもimplicit修飾できない)。複数ある場合は、オーバーロードと同じ方法で最適なものが選ばれる。

この機能はビューと同じく暗黙名空間を利用するものではある(ビューの暗黙名空間も上と同じ定義である)が、それとはまた異なるものであることに注意されたい。


前ページ | トップページ | 次ページ