kmizuの日記

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

Scalaの学習コストを下げるための心得

追記Twitterで、「それって、言語マニアにしかできない技のような気が」という指摘を受けました。自分としては一般的に適用可能な話だと思っていますが、あるいは自分の感性が著しくずれているのかもしれません。その辺承知の上でお読みください。

Scalaは習得が難しい言語だ、とよく言われます。また、実際問題として、Scalaの言語仕様の全体はそれなりに複雑でもあります。しかし、それはたとえばJavaでも言語仕様の全体像を把握するのは難しい話であり、Scalaに限った話ではありません。にも関わらず、Scalaの習得が難しいとよく言われるのはプログラミング言語の学習モデルが誤っているからではないかと最近思うようになりました。そこで、Scala(や他の言語も含めて)のコストを下げるために必要な心得についてちょっと書いてみます。

これは、Scala関数型プログラミング言語である、という主張と相反するように見えますが、オブジェクト指向関数型プログラミングは特に対立するものではありません。一方で、Scalaの核となる部分は、従来のオブジェクト指向言語(たとえばJava)の発展系であり、Scheme/ML/Haskellといったラムダ計算をベースとする言語とは明らかに異なります。Scalaは核となるオブジェクト指向言語関数型プログラミング言語から輸入した機能を取り込んだ言語と見るべきです。ですから、Scalaを使う際にいきなりパラダイム・シフトする必要はありません。必要になったときに理解すればいいのです。

  • 文法の一般形と省略形を区別する

たとえば、Web上にあるScalaのメソッドの文法解説として、

def メソッド名(引数: 引数の型, ...): 返り値の型 = { //返り値の型は省略可能
  return hoge // return は省略可能
}  // 単一行の場合は{}を省略可能

のようなものを見ますが、これは誤りです。正しくは、

def メソッド名(引数: 引数の型, ...): 返り値の型 = 式 // 返り値の型は省略可能
// { } は要素を順に評価し、最後の要素の評価値を返す式

です。この二つで何が違うかというと、前者は本来省略形でないものも省略形と認識してしまうことによって、余計なことを覚える必要があるという点です。

また、クラス継承において、継承元が一個の場合はextends、二個以上の場合はそれに加えて、withが必要という解説もありますが、これも厳密には正しくありません

class A extends (Super1 [with ....])

が正しい一般形です。Scalaの文法は、一般形を正しく認識すればそれほど複雑ではない部分が多いのですが(プレースホルダ構文は例外的に複雑です)、省略形と一般形とを誤って認識すると途端に覚えることが増えてしまいます。

  • 式を基本として考える

Javaなどから来るとやや異質に感じるかもしれませんが、Scalaでは制御構造も含めてほとんどの「いわゆる文」が式です。式は評価すると必ず(例外が投げられない限り)何らかの値を返します。

たとえば、

if(1 < 3) {
  println("1 < 3")
}

と書いたとき、この式の返す値の型はUnitです。また、

a match {
  case 1 => "1"
  case 2 => "2"
  case 3 => "3"
  case _ => "otherwise"
}

という式が返す値の型はStringです。そして、式の評価値は変数に代入できるので、当然のことながら(match式に型が付く場合)

val b = a match {
  case 1 => "1"
  case 2 => "2"
  case 3 => "3"
  case _ => "otherwise"
}

は基本的にコンパイルを通ります(例外はありますが)。

  • 「関数」と「メソッド」を区別する

これは、コップ本とかScala言語仕様ですら正しく用語を使い分けしていないのが悪いのですが、Scalaにおいて

  • 関数は第一級の値である
  • メソッドは第一級の値でない

という根本的な違いがあります。そして、あるものが関数かどうかを判定するのはとても簡単で、ぶっちゃけていうと、defを使って定義されたもののみがメソッドで、それ以外が関数です。

  • Implicit Conversionは本質的な機能ではない

若干語弊があるのですが、そもそも機能としてはImplicit Parameter(によって実現される機能)が本質的であり、Implicit Conversionはその特殊な形です。より正確には、A => Bという型のImplicit Parameterがあったときに、これはA型からB型へ暗黙に変換ができるとみなしてコンパイラがコードを挿入するというだけの話です。Implicit Conversion怖いという意見をよく見るますが、そもそもそれは本質的なものではないのだ、ということです。

考えながら項目を継ぎ足していったのでイマイチ整理しきれていませんが、まとめとしては、核となる機能と枝葉の機能を区別せよ。そして、枝葉の機能は可能な限り核となる機能への変換としてとらえよ(そうすれば覚えるべきことを最小化できる)、ということになるかと思います。このアドバイスはScalaだけでなく新しい言語を高速に覚える際に役立つと私は思っています。

自分流のプログラミング言語学習法をまとめた感じなので、異論があるかもしれませんが、「個々の機能を丸暗記する」学習法は効率が悪いのだ、ということは強く言いたいです。