読者です 読者をやめる 読者になる 読者になる

kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Scalaのメソッドや関数に関するQ&A

Scala勉強会第170回 in 本郷

rpscala.doorkeeper.jp

は、サブテーマ「Scalaの言語仕様」であったため、久々に熱弁をふるったところ、特に、メソッドや関数の仕様や区別に関して疑問に思った方が多かったらしく、質問も多かったので、Q&A形式でまとめておきます。

Q: (x1, xN) => body 形式と、{ case pat1 => body1; ... case patN => bodyN }形式の違いは何でしょうか?

A: 前者は必ずFunctionN[S1,...,SN,R]型を持つのに対して、後者は期待型(expected type)によって型が異なります:

1: FunctionN[S1,...,SN,R]: この場合、

(x1:S1,...,xN:SN) => (x1,...,xN) match {
  case pat1 => body1
  case ...
  case patN => bodyN
}

と同じ意味になります。通常の無名関数構文+本体で直後にパターンマッチングを行うわけです。

2: PartialFunction[S, R]: この場合、

new scala.PartialFunction[S, R] {
  def apply(x: S): TT = x match {
    case pat1 => body1; ... case patN => bodyN
  }
  def isDefinedAt(x: S): Boolean = {
    case pat1 => true  .. case patN => true
    case _ => false
  }
}

と同じ意味になります。期待型がPartialFucntion[S, R]の場合、パターンにマッチするかだけをチェックすることができる、isDefinedAt()メソッドが自動的に定義されるのがポイントです。Scalaのコレクションクラスのメソッドcollectはこれを利用して実装されています。

scala> List(1, 2, 3, 4).collect{ case x if x > 2 => x * 2}
res0: List[Int] = List(6, 8)

collectcaseにマッチする要素だけを取り出して、かつ、関数本体を適用した要素からなる値を返します。いわば、filter + mapのような動作を行うわけですが、caseにマッチするかだけをチェックできるため、このような挙動が可能になるのです。

3: FunctionN[S1,...,SN,R]に対してSAM-convertibleである:この場合、

(x1:S1,...,xN:SN) => (x1,...,xN) match {
  case pat1 => body1
  case ...
  case patN => bodyN
}

と同じ意味になります。これは、Scala 2.12で正式に導入される(Scala 2.11でも実験的なオプションを使えば利用可能な)Single Abstract Methodだけを持つインタフェースへの変換を念頭に置いたものだと思われます(これは、昨日説明していて言語仕様を参照するなかで初めて気が付きました。先行して、仕様を記述しておいたのでしょうか?)。

Q: タプルを引数にとる関数と複数の引数をとる関数の違いは何でしょうか?

A: 前者はFunction1[(S1,...,SN),R]型を持つのに対して、後者はFunctionN[S1,...,SN,R]型を持ちます。特に、JVMレベルでは、前者は型消去の後は全て同一の型になるのに対して、後者は引数の数に応じて異なる型を持ちます。

この区別は、JVM上では、複数の引数を取る関数を別に扱った方が効率的な実現が可能になるからです。効率を無視すれば、全てFunction1型で表現するような実装も可能だったと思われます。ただし、その場合、2引数以上の全てのメソッド呼び出しのたびに、タプルの生成、タプルからの値を取り出す余分なコードの実行が必要になります。この辺は、JVM上で実現する上での妥協の産物といえます。

なお、MLやOCamlなどの言語では、この区別が存在せず、タプルを引数に取る関数しか存在しませんが、だいたいの処理系では、タプル引数の関数を効率的に実行できるようになっているはずです。

Q: _の使い方がよくわからないので、教えてください。

A: パターンマッチのパターンを除けば、式中のおおざっぱに言って、次の二通りにわかれます。

1:

val abs = Math.abs _

のように、メソッド に対して、空白を一つ以上あけて_を付けるケース。この場合、メソッド型(ユーザが書き下すことができない)から関数型への変換を指示する構文になります。たとえば、Math.absメソッド型は

(Int)Int

というものになりますが、これはファーストクラスの型ではなく、そのままでは値を引数として渡したり、変数がそれに束縛されたり、返り値として返したりすることができません。Math.abs _とすると、

(Int) => Int

型になります。この型はファーストクラスの型であり、その値を引数として渡したり、変数がそれに束縛されたり、返り値として返したりすることが自由にできます。

2:

List.map(_ + 1)

のように、突然_が出現する場合。これは、プレースホルダ構文と呼ばれ、構文解析時に特別な処理が行われます。詳しくは、私の過去のブログエントリ

kmizu.hatenablog.com

をご覧ください。この構文のめんどくささがよくわかります。

Q: メソッドと関数の違いがよくわかりません。

A: メソッドはファーストクラスの値ではありませんが、関数はファーストクラスの値です。

この呼び方は、厳密に言うとScala Language Specificationの呼び方と異なるのですが、こう考えた方がわかりやすいです。より厳密にいえば、メソッドメソッド型(ファーストクラスの型ではない)を持ち、関数は関数型(ファーストクラスの型)を持ちます。メソッド型は_構文で、対応する関数型に変換することができます(eta-expansion)。

構文的な区別としては、defで定義されたものは全てメソッドであり、それ以外が関数であると考えて間違いありません。

また、メソッドジェネリック(多相的)になれますが、関数は多相的になることができません。

trait PolymorphicFunction {
  def apply[T, U](arg: T): U
}

このPolymorphicFunctionapplyジェネリックメソッドですが、関数の型で同じことを表現することはできません(関数型の実際の表現はFunctionN[N1,...,NN,R]であり、関数の型が決まると型引数が全て決まってしまうため)。

なお、

List(1, 2, 3).foreach(println)

のように、一見「メソッドを値として渡している」ように見える箇所がありますが、これは、実際には

List(1, 2, 3).foreach(println _)

とほぼ同じで、メソッドの型を関数の型に変換せよと指示しているのであり、メソッドをそのまま渡しているのとは異なります。

とりあえず、こんな感じです。だだっと書いたので、何か見落としなどあるかもしれません。その際はご指摘よろしくお願いします。