kmizuの日記

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

Kotlinの謎(解明編)

今日書いた

kmizu.hatenablog.com

についてですが、原因がわかりました。まずは以下のコードを見てください

Welcome to Kotlin version 1.0.0 (JRE 1.8.0_91-b14)
Type :help for help, :quit for quit
>>> val a: () -> Int = { 2 }
>>> val b: () -> Unit = { 2 }
>>> println(a())
2
>>> println(b())
kotlin.Unit

変数abの右辺は共に{ 2 }になっており、これは0引数の無名関数を表しています。一方、aの型は引数なしでIntを返す関数、bの型は引数なしでUnitを返す関数となっており、型に互換性がないにも関わらず、代入することができています。

このことから、無名関数の式の型は、期待される型によって(も)決定されるのだろうということがわかります。具体的には、2行目の{ 2 }の型は期待される型が() -> Unitなので、{ 2 }自体が() -> Unit型になり、そこから、

{ -> 2; Unit }

のようにUnitが補われているのだろうと思います。

F.kt:3:25: warning: the expression is unused
  val b: () -> Unit = { 2 }
                        ^

という警告が出る辺りもそれを裏付けていると言えそうです。

Kotlinの謎

Kotlinという言語の型システムは凄くおおざっぱに言ってしまうと、Scalaと非常によく似ています。

  • TopがAny
  • BottomがNothing
  • Byte, Short, Int, Float, ....といった、プリミティブ型相当の型が継承階層に組み込まれていて、型名まで同じ
  • Generics
  • Declaration-site variance
  • Use-site variance

違う部分、たとえばnullabilityを型システムレベルでサポートしてるとかはもちろんあるのですが、AnyとNothingという名前まで真似してる辺り、非常にScalaの影響が大きいのは間違いありません。

一方で、Kotlinがあえて採用しなかったものの一つに、暗黙の型変換があります。たとえば、Scalaでは、任意の型はUnitに暗黙に型変換できます。そのため、たとえば

val x: Unit = 1

というコードはコンパイル可能です。一方で、Kotlinは

val x: Unit = 1

というコードはコンパイルを通りません。これはまあ設計判断としてはどっちもありだと思います。

さて、それはいいのですが、一つ妙な点があります。次のプログラムを見てください:

gist.github.com

runは引数として渡された0引数無名関数を呼び出し、その最後の評価値を返すメソッドです。ここで、run<Unit> { 1 + 1 }とすることで、1 + 1の結果を強制的にUnitに変換しようとしています。普通に考えると、IntからUnit への暗黙の型変換が駄目なのだからコンパイルも通らなそうなのですが、何故かコンパイルを通ってしまいます。

これはKotlinが暗黙の型変換をサポートしていないという事実と反しているようにも思える。あるいは、実装のバグなのかもしれません。というわけで、Kotlinに詳しい方、何故これがコンパイルを通るか(どういうコードが生成されるかはわかっているので、仕様的に何故通るべきなのか)教えていただけないでしょうか(教えてクン)

Macro PEG 0.0.10 リリース

かなり久しぶりのリリースです。主な変更点は、

Conflict between an expression parenthesis syntax and call syntax · Issue #16 · kmizu/macro_peg · GitHub

を修正した点でしょうか。これで、

STRING = STRING_MACRO("\"") / STRING_MACRO("'");
STRING_MACRO(Q) = Q (!(Q / "\\") . / "\\" .)* Q;

のようなPEGにおいて、Q (!(Q / "\\") が正しく Q(!(Q / "\\") の連接として解釈されます(今までは Qの呼び出しと解釈されていた)。修正方法としては、マクロ名につづいてスペースが続かずに引数の括弧が続く場合はマクロ呼出し、スペースが挟まれている場合は連接として解釈するようにしました。当初、グルーピングは[]という別の記号を使おうと思いましたが、記述として気持ち悪いので、現状のシンタックスをあまり変えないようにしました。

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だけでなく新しい言語を高速に覚える際に役立つと私は思っています。

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

これまでの人生で一番、死を身近に感じたときのこと

とても物騒なタイトルがついていますが、別に自殺しかけたとか事件に巻き込まれたという話ではありません。

大学1年生の夏(2002年)の事でした。当時、筑波大学1年生だった自分は、サークル(?)の先輩たちとともに、今ではガルパンの聖地として知られている大洗まで、筑波大学から自転車で日帰りで行くことになっていました。なんでそうなったかはよく覚えていないのですが、たぶん自転車で海まで行こう!といういい加減なノリだったと思います(まあ大学の学部生なんてそんなもんですよね)。ちなみに、自転車とは行ってもママチャリで、しかも片道42kmくらいあるので今思えばなかなかハードな行程でした。

さて、そんなハードな行程にも関わらず、帽子もかぶらず、汗を拭くものも持って来ないという大馬鹿なことをしたのがそもそも間違いでした。集合したときに、帽子をかぶってないこと等について、大丈夫?という旨の心配されたのですが、自分は能天気にも大丈夫だろうと思って随分軽い返事をしたのを覚えています。

そんな感じでハードな行程に見合う準備をせずに大洗への日帰り旅行は始まったのですが、案の定、途中でバテ始めました。先輩たちは気のいい人で、途中でギブアップすればすぐ応じてくれただろうと思うのですが、当時の自分は、サークル(?)に入ってあまり経っていないこともあり、心配をかけたくないという思いでなんとかついていきました。大洗は海水浴客で賑わっている中、とても暑いなーと思ったことが印象に残っています。

なんとかかんとか筑波大学に戻って来ることが出来、解散して帰宅したのですが、問題はその後でした。気がついたらすっかり発汗が止まり、身体が熱っぽくなっていたのでした。途中で水分補給はしたものの、たぶん水分が不足しているのだろうと近くのコンビニでスポーツドリンク2lを買い、がぶがぶ飲んだのですが、汗はちっとも出ないし尿もでない。これは何かおかしいと思って体温計で体温を測ってみると38度超。ここに至ってもまだなんとかなるだろうと思い、さらに水分を補給しながら体温の上昇が止まるのを待ったのですが、ちっとも止まる気配がない。ついには39度を越し、手足のしびれが来るに至って、いよいよこれはヤバイと思い、熱中症について検索しました。すると、どうやら熱疲労と熱射病の間くらいであり、このまま放置すると死ぬ!こんなところで人生が終わるなんて嫌だ!という強い恐怖感にかられ、急いで救急車を呼びました。

結果、熱中症と診断され、医師に「このまま放置していたらおそらく死んでいたよ」と言われ、心底ぞっとしたのを覚えています。そして、何の数値かは忘れたのですが、たぶん臓器の機能に関する何らかの数値が下がるまで入院となったのでした。先輩や友人、弟が見舞いに来てくれたのをよく覚えています。

それ以来、夏に熱中症の前駆症状のような症状が現れたらすぐ気づくようになり、すばやく水分補給とタオルで頭を冷やすなどの対処ができるようになったのでした。怪我の巧妙功名というやつですね。また、それまでも割と死への恐怖は強い方だったのですが、この体験を経て死への恐怖は一層強化されたのでした。結論はないですが、熱中症には気をつけましょうということになるでしょうか。

Scalaアンチパターン:変更可能コレクションをvarとして宣言する

Scalaは最初から関数型プログラミングのスタイルで書くことを意識して設計されたという意味で関数型プログラミング言語と言えますが、一方で、「Better Java」な手続き型スタイルで書くことも基本的には否定されるべきではないと思います。たとえば、ビッグデータ関係のソフトウェアとして有名なApache SparkはScalaで書かれていますが、(おそらく)パフォーマンス上の理由のため、手続き型のスタイルで書かれている部分がかなり多いです。

さて、それはいいのですが、そういう「Better Java」なScalaコードによく見られるパターンに、変更可能コレクションを使っているのに、そのコレクションを格納する変数をvarとして宣言しているものが(割と有名なソフトウェアでさえ)あります。ですが、そういうコードは 書いてはいけません(かなり例外的な場合を除いて)。

「書いてはいけません」というのはかなり強い言い方ですが、きちんと考えると割と当然の話です。

各要素が"name:age"で区切られたコマンドライン引数列を元に、Personクラスのオブジェクトの列を作り出す手続き型コードを考えてみます。たとえばこんな感じになるでしょう(書きたい処理の例がかなり無理矢理ですがご容赦ください):

import scala.collection.mutable.ArrayBuffer

case class Person(name: String, age: String)
var persons = ArrayBuffer[Person]() //valにしても意味は変わらない!
for(arg <- args) {
  val Array(name, age) = arg.split(":")
  persons.append(Person(name, age))
}

/*
 * personsを使って何かする(が、personsには再代入されない)
 */

このコードにおいて、ArrayBuffer[Person]型のpersonsを変更可能な変数として宣言していますが、これはvalにしても同じです。なぜなら、personsの「指す先」であるArrayBufferそのものが変更可能だからです。

もちろん、personsに変更可能コレクションを再代入したいなら、変数自体を変更可能にする必要がありますが、かなり考えづらい事態です。コレクションを空にしたいという要求がある場合でもclearメソッドを呼び出せば済む話です。

Javaでは、変数をfinalにするのは「追加で」finalを付加する必要があり、面倒くさいのであえてfinalをつけないということは割とよくありますが、Scalaは、変数の宣言時に、valとvarの「どっちかを選択させる」タイプの言語です。つまり、varで変数を宣言した場合、その変数 そのもののの値を変更する 意図があるとみなして良いのです。これは、コードを読むときの労力を削減する役割を果たします。

しかし、変更するつもりもない変数をvarにするという無意味なことをすることがまかり通ると、せっかくの良い言語設計も台無しになってしまいます。

というわけで、そういうことをしてはいけません。

あるいは、そういうコードを書いている人は、変数自体の変更可能性変数の参照先の変更可能性 の区別がついていないのかもしれません。Javaでも「初学者」がよくはまるポイントです。ですが、その区別がついていない人はまともなJavaプログラマですらないような気がします。

個人的に好き(な/だった)技術書籍10選

プログラマのための技術書籍○選」とかよく見るじゃないですか。あれ、自分はどうにも好きになれないというか、後進を自分がたどってきた道に引きずり込みたいだけじゃねーかと思うのが多いです(全部がそうだとは言いません)。なので、今回のエントリでは、あくまで、「私が好きな」技術書籍を10冊紹介することにします。単に思い出深いというだけの書籍も入っており、間違っても「オススメ書籍」ではないのでご注意を。

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 方法論・実践 (IT Architects' Archiveクラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 方法論・実践 (IT Architects' Archiveクラシックモダン・コンピューティング)

最初は、メイヤー先生の『オブジェクト指向入門」第二版。ちなみに第一版は今からすると内容が古すぎるので読むのはやめた方がいいです。この本の何が面白いって、メイヤー先生の強烈な思想がそこかしこから伝わってくるところです。特に、序盤の方の、「方法論」批判はとても痛烈で、「どうしても必要な場合にのみそれを使うこと」といった謎のアドバイスに疑問を持ったことのある方は読む価値があります。

ただまあ、DbCとかのコンセプトを既に知っている読者にとっては退屈な部分も多いであろうことも一方で否定しません。メイヤー先生の独特の語りが好きな人は読みましょう。ちなみに、この本、タイトルに偽りありの代表格であり、全然オブジェクト指向入門ではありませんし、メイヤー先生のOOPの定義はやや独特なので(ただし、本文中でちゃんと「定義」しているのがエライところ)、その点は注意です。

次は、Date先生の「データベース実践講義」です。この本もタイトルに偽りありというか、まったくもって現在のいわゆるRDBMSを扱うために有用な知識は身につきません。この本は読み物として読むのが正解というか、Date先生のSQL & 今のRDBMSディスり芸のために読むのが正解な気がします。といっても、Date先生、リレーショナルモデルの祖であるCoddと一緒に働いていた経験があるだけあって、いい加減なことを書いているわけではなく、現在のいわゆるRDBMSSQLがいかにリレーショナルモデルからかけ離れているかを口を酸っぱくして語ってくれます。この本でいわゆる現在のRDBMSSQLを扱うのに必要な知識は「ほとんど」身につきませんが、本来のリレーショナルモデルと現在の実装との差異について知りたい方にとっては色々発見があるのではないでしょうか。

Parsing Techniques (Monographs in Computer Science)

Parsing Techniques (Monographs in Computer Science)

超マイナーな上に日本語訳もされていませんが、次はParsing Techniques 2nd Editionです。この本、Parsing Techniquesとタイトルにあるのは伊達ではなく、LLやLR、CYKといった古典的なパーザはもちろん、PrologのDCGや、recognition based grammar、parallel parsing、boolean grammarといったマイナーな(?)話題に至るまで、構文解析に関する話題のかなりをカバーしています。一家に一冊あると安心できる、そんな書籍です。

Programming in Scala: A Comprehensive Step-by-step Guide

Programming in Scala: A Comprehensive Step-by-step Guide

おなじみコップ本の原著です。あえて原著を選んだのは、最初にこの本を読んだのが原著であり、また、初めてのScala DaysでOdersky先生にサインをもらったという思い出深いものだからです。内容についてはコップ本の書評をみれば色々あるでしょうから割愛します。

C言語ポインタ完全制覇 (標準プログラマーズライブラリ)

C言語ポインタ完全制覇 (標準プログラマーズライブラリ)

次は前橋和弥さんの「C言語ポインタ完全制覇」です。C言語のポインタの文法がどうにもよくわからんなーと頭をひねっていた大学1年生の頃、この本を読んだことがその後のC言語の理解にとてもとても役立ちました。内容については記憶している限りではANSI C 89ベースなので、今だとやや古い記述もありますが、大筋で今でも役に立つ内容だと思います。

Java 謎+落とし穴 徹底解明 (標準プログラマーズライブラリ)

Java 謎+落とし穴 徹底解明 (標準プログラマーズライブラリ)

次も同じく前橋和弥さんの「Java 謎+落とし穴 徹底解明」です。実は、この本を読む時点で、既に書いてある内容についてはあらかた知っていたので、前橋さんのJavaに対する語りを読みたくて買ったようなものです。まあ、正直言うと、この本に書いてある落とし穴の大部分は現在のJavaだと克服されていますし、克服されていないものも、まあ慣れれば大したことがないものが多い気がしますが、前橋さんのJavaディスりが面白く、ついつい読みふけってしまったのを覚えています。

Bruce EckelのJavaプログラミングマスターコース―徹底探究!Javaのしくみとオブジェクト作法〈上〉

Bruce EckelのJavaプログラミングマスターコース―徹底探究!Javaのしくみとオブジェクト作法〈上〉

  • 作者: ブルースエッケル,Bruce Eckel,安藤慶一
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 1999/10
  • メディア: 単行本
  • 購入: 1人 クリック: 8回
  • この商品を含むブログ (5件) を見る

Bruce EckelのJavaプログラミングマスターコース〈下〉徹底探究!Javaのしくみとオブジェクト作法

Bruce EckelのJavaプログラミングマスターコース〈下〉徹底探究!Javaのしくみとオブジェクト作法

  • 作者: ブルースエッケル,Bruce Eckel,安藤慶一
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 1999/10
  • メディア: 単行本
  • 購入: 1人 クリック: 2回
  • この商品を含むブログ (5件) を見る

これは良書かというと必ずしもそうではないし、内容も今となっては古いですが、自分を本格的なプログラミングの道に誘ってくれた忘れがたい書籍です(ちなみに著者のBruce Eckel氏は今はもうJavaを見限った(?)ようで、Atomic Scalaという書籍を出したりしています。ご本人にScala Daysで会ったりもしました)。この本でデザインパターン病にかかったのもとても思い出深いです。ちなみに、デザインパターン病はOOPを学ぶ過程でかかる人が多い病で、学生のうちに経験しておくと予後不良にならずに済みます。

Introduction to Algorithms (MIT Press)

Introduction to Algorithms (MIT Press)

  • 作者: Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest,Clifford Stein
  • 出版社/メーカー: The MIT Press
  • 発売日: 2009/07/31
  • メディア: ペーパーバック
  • 購入: 5人 クリック: 90回
  • この商品を含むブログ (19件) を見る

これは、邦訳が既に出ていますが、自分が研究室の輪講でやったのは原著だったので、原著を掲載しています。"Introduction"とついていますが、日本語の入門本にあるようなテキトーな感じのものではなく、様々なアルゴリズムについて、割と詳しくみていく本です。入門系の本としては、計算量を"証明"する課題などがあるのは割と珍しいかもしれません(英語の本だとよくありますが)。この本、研究室の輪講の期間では終わらなかったので正直言って紹介するのは気がひけたのですが、アルゴリズムに対する自分のアプローチのあやふやさを認識することになったという意味で良い本でした。

Purely Functional Data Structures

Purely Functional Data Structures

知る人ぞ知る名著、Purely Functional Data Structures(通称PFDS)です。これについては、k.inabaさんの(半分)ネタ解説を読む方がよっぽど面白いのでここでの紹介は割愛します。

詳説 正規表現 第2版

詳説 正規表現 第2版

これも思い出系ですが、日本語の正規表現解説書籍としては一時期、かなり一般的だったのではないでしょうか。大学2年の頃、簡易正規表現エンジンを作ってみようとして(そして作りました。NFAベースのごく簡単なマッチャですが)買ったのがこの本だった記憶があります。様々な処理系における正規表現の違いや、NFA、POSIX NFA、DFAの違いなど、一通り解説してあります。

今だと、「正規表現技術入門」

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

の方がいいかもしれません。

というわけで、あやふやな記憶を頼りに、思い出深い技術書籍を10冊無理やり紹介してみました。