kmizuの日記

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

新しい言語を覚えるために私がした事(Kotlinの場合)

先日の、Scala勉強会第170回 in 本郷 : サブテーマ「Scalaの言語仕様」

rpscala.doorkeeper.jp

Scalaの言語仕様について解説していたときの反応をみて、どうも、自分のプログラミング言語の把握の仕方はあまり一般的ではないのではということを考えました。どう違うかというと一言では説明できないのですが、世間的には、プログラミング言語については、よりフィーリング的になんとなく理解している部分理解していない部分がぼやーっとしているのに対して、自分の場合、理解している部分とそうでない部分の境界がくっきりしているような感じです。

それはともかくとして、このエントリでは、自分が最近新しく触った言語であるKotlinについて、どのようにして理解を進めたかを書いてみたいと思います。

公式ドキュメントを読む

定番といえば定番ですが、公式ドキュメントが一番正確に言語について書いてあるものです。Kotlinの公式リファレンスは以下から見ることができます。

kotlinlang.org

今まで色々な言語を触ってきましたが、その中でもKotlinのリファレンスはよく書かれている方だと思います。ですが、ここではそういうことは割とどうでもよくて、とにかくサンプルコードをざっと眺めて、自分の脳内におおざっぱに文法定義のようなものを構築します。この時点ではBNFほどくっきりとした形ではないですが、

  • (クラス|オブジェクト)は任意個のメンバー定義からなっている
  • メンバーの型名は省略可能
  • メソッドが単一式からなる場合、`='に続けて本体を記述する(注:便宜上メソッドと呼んでいます)
  • メソッド複数の式を含むブロックからなる場合、'='ではなく、直後に{ ... }を続ける

といった事実を積み上げて、全体としてどのような文法になっているかを推測していきます。文法について疑問が湧いたときは、「こうだろう」というおおまかな仮説を立てて、それが正しいかを検証します。言語のリファレンスは文法について網羅的に解説していませんから、コンパイラに聞くのが手っ取り早いです。そこで、コーナーケースを含む入力をコンパイラに食わせてパーズエラーになるかどうか等を調べます。

パーザコンビネータライブラリを作成

一見ネタっぽいですが、これは結構馬鹿にできない効果があると思います。Hello, World!から学べることは極めて限られていますが、パーザコンビネータライブラリを作ると

  • ジェネリックスを含む型システム
  • 文字列の基本操作
  • 関数の取り扱い(関数を第一級オブジェクトとして扱う場合も含む)
  • クラスやオブジェクトの取り扱い
  • etc

など様々なことを一度に学べます。

公式ドキュメントの疑問点を突き詰める

これは、公式ドキュメントの説明が不足していると感じた場合によく行う方法です。たとえば、Kotlinでは、smart castがあるよというのを謳い文句にしてますが、どのくらいsmartなのかについて説明がありません。

val s: String? = ...
if(s != null) {
    println(s.length)
}

といったサンプルを見せられても、じゃあ、たとえば

if(false || s != null) {
    println(s.length)
}

はOKなのかとか、

if(!!!!!!(s != null)) {
    println(s.length)
}

はOKなのかとか、色々疑問が湧いてきます。ここで重要なのは、完璧なスマートキャストといったものは基本的に不可能なので、型システムにおいて一般的な、「安全側に倒す」(=not-nullableと判定された場合は必ずnot-nullableだが、全てのnot-nullableを検知できるわけではない)手法を取っているに違いないという推測を働かせることです。そして、「どの程度の近似精度なのか」を確かめるために、色々なプログラムを食わせてみます。

kmizu.hatenablog.com

はそういった事を調べる過程でわかったことを書き留めたものです。

また、Kotlinの公式ドキュメントによれば、ブロックからなる関数定義は、必ずreturnを書かなければいけなくて、その根拠として

Kotlin does not infer return types for functions with block bodies because such functions may have complex control flow in the body, and the return type will be non-obvious to the reader (and sometimes even for the compiler).

というのが書かれていますが、ラムダ式複数の式を持てるのにreturnを書かなくて良いことを考えると、このドキュメントの記述はおかしいです。これを立証するために、次のようなblock関数を定義し、実際に、複数の式からなる関数定義においてreturnを不要にできることを確認しました。

inline fun <T> block(body: () -> T): T {
    return body()
}

この辺で意識していたのは、公式ドキュメントのサンプルコードは信用するが、説明については話半分くらいに思っておくという態度です。言語仕様書であれば正確を期していることが期待できますが、言語の公式ドキュメントは、魅力的な機能を新規ユーザに対してアピールする場でもあり、しばしば正確さは犠牲になります。

さらにサンプルプログラムを書く

この時点で、言語の構文や型システムについてある程度把握できていましたが、より深く理解したいので、さらにいくつかのサンプルコードを書いて実験してみました。できるだけ、言語のシステムのコーナーケースを付くようないやらしいサンプルコードを考え、それを通してさらに理解を深めます。

ソースを読む

だいたいの言語処理系のソースにはBNFによる文法定義がついてくるので、なんだかよくわからない構文が登場したときはそれを読めばだいたい解決できます。それでもわからない場合、実際に字句解析器や構文解析器のソースまで読みます。今回の場合、字句解析器のソースは読んだものの、構文解析器は面倒そうだったのであえて読みませんでしたが、その気になれば読めると思います。

型チェッカはフロー解析を行う都合上、通常の静的型付き言語よりややこしそうだなと思ったので、今のところソースは読んでいません。正直、Kotlinのフロー非依存の部分の型システムは大体わかったと思うのですが、フロー依存の部分についてはちゃんと理解していません。ですが、「フロー非依存の部分については理解できた」「フロー依存の部分については未理解の部分が多い」といった形で、理解していることとそうでないことの間に境界線を引いておきさえすれば良い話です。

まとめ

だらだらと書いてきましたが、

  • コンパイラ作者の気持ちになって言語仕様を推測する
  • 文法の全体像を思い描きながら、随時修正する
  • 理解していることと理解していないことの間に適切な境界線を引く

辺りが自分にとっての重要なポイントなのではないかと思いました。特に、最後の点は個人的に一番重要な点です。理解していないことの範囲が適切にわかっていれば(あるいは理解していない範囲をくくり出せれば)、プログラミング言語の全体像についてわかっていないことを恐れる必要はないためです。

なお、当たり前ですが、Kotlinについてよく理解したからといって、それでKotlinで実用的なプログラムを書けるようになるわけではありません。