kmizuの日記

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

Javaのジェネリクスは「まがい物」ではない

先日、自分が書いた

kmizu.hatenablog.com

に対する反応として、「Javaのようなまがい物のジェネリクスと比較するのは適切でない」「Javaジェネリクスと比較するのは適切でない」(おそらくC#や(C++等(2017/09/24追記))の言語と比較して)といった コメントをいくつか見かけました(はてなブックマークコメントやツイッターなどで)。しかし、ここでは、そのような言説こそが適切でない、ということを言いたいです。なお、methane氏との 対話については既に終わったものなので、それとは関係ありません。

そもそも、Javaジェネリクスは、静的型付き関数型言語で既に一般的であったパラメータ多相をJavaに追加する目的で導入されました(C++テンプレートのようなものをJavaに追加するためだと思っている人がいるかもしれませんが、それは実態にあっていません)。実際、Java Genericsの前身のGeneric Javaのさらに前身であるPizzaの論文で、Martin Odersky*1は、PizzaはJavaにパラメータ多相(parametric polymorphism)などを導入するものであることを明言しています。

この、パラメータ多相という用語は、型理論の分野で広く使われている用語で、型によってコードをパラメータ化できるという性質を指します。重要なのは、パラメータ多相(ジェネリクスと同義)では、そのような、型によってパラメータ化されたコードを表現できて、なおかつその整合性を静的にチェックできるという点が本質であって、 型パラメータが実行時に取得できる とか、 特定の型引数に対して特殊化できる とかいった性質は、あくまで 実装方式 によって得られる追加のメリットであるということです。そして、追加のメリットがないというだけで、Javaジェネリクスが紛い物だとか主張するのは、JavaC#、あるいはJavaC++とかしか知らない人が自らの無理解を示しているに過ぎないし、噴飯モノの言説だということです。

また、実行時に型パラメータ情報を持ったり、特定の型パラメータに関して特殊化できるというのは便利ですが、ランタイムの実装を大幅に複雑化します(実行時にもパラメータ多相を扱えなければいけないので)。一方で、実行時に型パラメータ情報を持たなければ、ランタイムの型システムをある程度無視して言語の型システムを拡張しやすいです(Scalaが典型例)。

別の例としては、Javaは2016年に、型システムが健全でないことが証明されたり型チェックが決定不能であることが証明されたりしていますが、ランタイムの型システムは別なので、これらの問題について、ランタイムが影響を受けることはありません。一方で、C#をはじめとした.NET言語では、言語の型システムとランタイムの型システムがかなり近いため、言語の型システムに欠陥が見つかると、それが直接ランタイムに影響を与えるおそれがあります。

このように、ジェネリクスの実装方式はそれぞれ得失があるので、どちらがいいとすぐ決めつけられるものではありません。両方の性質についてきちんと理解した上で、どちらの方式を支持するか決めるのはもちろんいいですが、その前に色々勉強すべきことがあると思います。

以下、2017/09/24追記

特定の型パラメータに関して特殊化できる、というのの意義が伝わっていなかったので書くと、

class G[T] {
  def puts(x: T) = println(x)
}

があったとして、

G[Int]

に対しては、

class G_Int{
  def puts(x: Int) = println(x)
}

のような効率的なコードが生成される(あるいはできる)ということを指しています。今回、別に実行時型情報のみを問題にしたわけではありません。

Javaのジェネリクスは「まがい物」ではない - kmizuの日記

RTTI(実行時型情報)は何の関係もないです。筆者は根本的なところで勘違いしていませんか?「Scalaジェネリクスだとforがインライン化できずに糞遅い」理由を考えてみましょう。JavaScalaのはただのCastのSugarです。

2017/09/24 10:58
b.hatena.ne.jp

上記の通り、特に勘違いしていません。また、Scalaforが遅い理由についても、何か勘違いされてると思います。キャストの構文糖衣だというような見方については、今回の本文を読んでくださいとしか。言語の型システムの話をしているのに、いきなり、実装としての、「キャストをはさむ」ことに言及されてるので、その二つの違いについてじっくり考えてみられると良いかと。

*1:Scalaの作者であり、GJ Compilerの作者

Re: Go にジェネリクスがなくても構わない人たちに対する批判について

先日自分がGoについてつぶやいたものが、id:methaneさんに捕捉されていて、それに対する反論記事

methane.hatenablog.jp

があがっていたので、それに対する所感を書いてみました。(2017/09/22 追記):cocoatomoさんから指摘があったのですが、引用元は全て id:methane さんの、上記ブログエントリの文章です。雑多な感想に関して反応しているものではないです。

前置き

Goや言語批判に関するスタンスを誤解されると嬉しくないので、最初に書いておきます。

  • Goの言語仕様はあまり好きではないけど、Goユーザーを見下したり、馬鹿だとかそういう風には思っていない

言語仕様批判とユーザー批判は別の話ですが、しばしばいっしょくたにされて、糞言語Xを使うユーザは糞だ、みたいな主張がされることがあるので、 そういう意図はないという表明です。

  • Goユーザーの中で、ジェネリクスがなくても構わないと主張するユーザーへの批判はしたけど、Goユーザー全てがそうだと思っているわけではない

  • Goユーザーの中でジェネリクス不要論を唱えているユーザーへの批判はしたけど、そういうユーザーを馬鹿にしているわけではない

これは掛け値なしの本音なのですが、そう思ってもらえるかどうかについては読み手にお任せするしかありません。

Goはマイクロサービス的なものを開発するための言語か?

Goは特に今で言うマイクロサービス的なものを(色んな意味で)効率よく開発するために作られた言語で、DBとかJSONとかRedisとか扱って HTTP API提供する簡単なサービスを書いてみたら分かると思うんだけど、本当にジェネリクスがなくて不便な場面ってメチャクチャ少ない。 interface {} (Javaでいう object) が出て来る場面って、ジェネリクスが無いせいで出て来ることは本当に稀。

とのことですが、まずこの前提がおかしいのではないかと思います。実際の利用シーンを視ても、コマンドラインツールやWebサービスGCありのC的な使い方、などなど、かなり様々な用途で使われている言語だと思いますし、Goが出たときのドキュメントにも、利用シーンをそれほど限定するような文言はなかったように思います。

ただ、その上でいうと、HTTPとかJSONとかを扱うとき、主にスキーマがないものを扱うため、ジェネリクスがあってもそんなに便利にはならないことが多い、ということはその通りだと思います。DBについては、RDBなら、ジェネリクスを使って、(スキーマの整合性がとれていれば)型安全であることを謳うフレームワークは少なからずあります。

ソート関数とジェネリクスの関係

唯一面倒、あるいは不格好だなぁと思うのはソートだ。ジェネリックなソート関数を使うのが面倒で、便利さのために sort.Ints() とか sort.Strings() とかを提供している*1のが 不格好なんだけど、この面倒臭さも単純にジェネリクスが無いせいとはいうわけでもない。

例えば Javaジェネリクスがプリミティブ型にそのまま対応したとしても、プリミティブ型が .compareTo() を持ってないし演算子オーバーロードとか「この演算子を定義している型」 を宣言する方法を持ってないから、ジェネリック関数に渡すには「値の比較方法」「値の交換方法」を教えてあげないといけなくて面倒だよね。

これは、部分的には正しいのですが、比較関数として、無名関数を使うケース(Java 8以降)だと、ジェネリクスがあるかないかで、比較関数の書きやすさが全く違います。

たとえば、Java 8以降で、整数のリストを降順ソートしたいとき、

Collections.sort(xs, (x, y) -> y.compareTo(x));

と書けば済みますが、ジェネリクスがないと

Collections.sort(xs, (Object x, Object y) -> ((Integer)y).compareTo((Integer)x));

となって面倒さが大幅にあがります。

余談ですが、比較関数を渡すのも面倒だという話はあって、そういう場合に使える言語機構として、Haskellなどにある「型クラス」があります。

Haskellは普段かかないので、Scalaで書くと、次のようになります。

val sortedList = List(3, 2, 4, 1, 5).sorted // 整数同士を比較するオブジェクトが暗黙の内に渡される

でも、Goがジェネリクス持ってないことを非難するときにはJavaを「持ってる」側に分類しておいて、Javaレベルのジェネリクスはあんまり嬉しくない*2ことを説明すると 「ジェネリクスJavaだけの機能じゃない。Javaと比較すんな」みたいな反応されるのにはうんざり。

Javaレベルでのジェネリクスでも上記のように十分うれしいと思いますが、それはともかく、私が言ってないことを勝手に補完してそれに反論されるのはちょっと困惑です。ここに関しては引用がないので、一般論としての反論なのかもしれませんが。

ジェネリクスを使う側と定義する側の嬉しさの混同

一方で今のGoユーザーは、 int v = (int)v.get(i) なんてコードはほぼ書かない。昔のJavaと違って、動的配列もマッピング型もジェネリックな組み込み型だから。たまに書く場合も、 JSONを map[string]interface{} で扱うとか、どんな型でも値として入れられる Context から値を取り出すとか、ジェネリックがあっても便利になるとは思えないケースがほとんどだ。

これは、汎用的な型を「定義」できることに関するメリットを見落としているように思います。Goは確かに動的配列もマップ(連想配列)も組み込み型なので、Javaとは事情が多少違いますが、それ以外のコレクションが欲しくなったときに、Goの組み込み型だけでは対応できません。代表的な例として、集合を表す SetJavaではジェネリックなコレクションとして提供されていますが、現状のGoでそれを実現しようとすると、必然的に要素型は interface{} になってしまって非常に嬉しくありません。

また、コレクションに対して map したり filter したりしてコレクション操作を楽に書けるようになているのが、今時の言語だと一般的だと思いますが(自分もそのように書くのに慣れてしまって、ループでコレクション処理を書かなければいけないのは苦痛です)、このようなジェネリックmap メソッドや filter メソッドを定義しようと思うと、ジェネリックな関数を定義する機能が必要になります。

さらに、標準のコレクションでは性能要求を満たせない場合や、不変コレクションが欲しい場合など、Javaではサードパーティのコレクション(有名なものだとEclipse Collection)を使うという選択肢があって、そのようなサードパーティのコレクションも標準と同じ様な使い勝手を提供できますが、Goではそのようなものを提供しようとすると、要素型がやはり interface{] になってしまいます。

他にも汎用的なグラフライブラリを提供するとき(JGraphT)など、頂点や辺の型は型パラメタとして定義されています。これによって、ユーザ定義のクラスを型安全に、頂点や辺の型として使えます。Goではやはり interface{} になってしまうのも言うまでもないことです。

Goの利用シーンを限定するなら、そのような欠点は大した問題にはならないかもしれませんが、一つの機能がないことで利用シーンを大幅に狭めるのはあまりいいことだとは思えません。

その他

だけど、実際にはジェネリクスなしで型安全を手に入れるために、「イディオム」というコストを払っている。例えばFIFOキューから値を取り出すなら、今は v := q[0]; q = q[1:] なのが、ジェネリクスがあれば v, ok := q.Pop() になるだろう。後者に慣れたら、きっと前者は面倒に感じるはずだ。

イディオム以前に、後者だと間違ったpop操作をする可能性がないのに対して、前者は間違った操作をする可能性があるので、単に面倒以上のコストを払っていると思います。本来なら必要なかった、コードのコピペというコストを払っている、といってもいいです。

頑なに要らないと言ってる人が具体的にどの発言のことを差してるのか分からないけど、コア開発者たちはツールチェインやランタイムの進化を優先していただけで 頑なに拒否してたりはしません。今はツールチェインやランタイムが大分進化したから、Goの適用範囲を広げるためにジェネリクスを含めて機能追加も検討し始めようかっていうフェーズです。

これは私に対する反論ではなく、mizchiさんへの反論ですが、正直この辺は非常に(開発チームの言っていることには)懐疑的です。そもそも、Goが公式に初登場した時期(2009年頃?)でも、ジェネリクスを入れることについては検討中といってたわけで、それから8年経った今でも検討中と言われても一体いつ入れるつもりなんだというのは疑問に思います(その間、いくつものGoにジェネリクスを入れるプロポーザルが登場しているにもかかわらず)。

逆に言語にこだわらない、Goの広い分野への布教に熱心でない人は、ジェネリクスが無いことが不便な場面ではわざわざGoを選ばないでC++C#JavaやRustなどを使ってるので、 本当にジェネリクスが無いことで困ってない。

これはソースが欲しいです。現状、Goの利用シーンはかなり広がっているのに、ジェネリクスが無いことで不便な場面で他の言語にスイッチするというコストを多くのユーザーは払っているのでしょうか?(便利ライブラリやフレームワークの有無が原因ならわかりますが)

全体として、利用シーンをあえて限定した上で、その範囲でならジェネリクスが無くてもあまり困らない、という主張にみえて、Goをそういう言語としてとらえるなら間違ってはいないと思いますが、私はGoをもっと汎用的な言語だと思っているので、その点が噛み合っていないのかなという気がしました。また、上の方でも書きましたが、ジェネリクスによって得られる柔軟性やメリットについてはちょっと過小評価気味ではないかなという気がします。

Scala関西Summit 2017に参加してきました!

実は今年が初参加なのですが、応募した発表、「プレースホルダ構文完全解説」が選考を通過したので、大阪まで行ってきました。

前日に、会社まで泊まりのための着替えなどを持っていき、会社が終わったら直行で新幹線で東京へ。@kawachi さんたちが手配してくださった、airbnbの宿に泊まりました。着いたのが22時過ぎで、集合写真

に載れなかったのがちょっぴり残念でした。

会場は、天満駅近くの天満研修センターというところで、なんと今年は4トラック同時進行!

私は全体的に疲れ気味で、控室で休んでいた時間が結構あったため、全部は聴けていないのですが、いくつか印象に残った点について。

  • エスカレーターで右に立つ

イベントとは関係ないのですが、新大阪駅から宿泊施設に向かっているとき、いつもの習慣で左側に立っていると、皆右側に立っていて、あれ…?と思いました。

  • エフ・コードの技術顧問就任について

会場でお会いした方にこの件について、色々たずねられました。おめでとうございます、と言っていただけることが多かったですが、自分としては、これから自分がエフ・コードさんに価値を提供していくことができるか、考えていかなければなあと思うところです。

  • null消しゴム

エフ・コードさんのブースで配布されていたやつで、

こういうものです。事前に、がくぞさん、麻植さんらと話し合ったときに、何がインパクトがあるかということで、この案が最終的に採用されたのですが、無事、当日に間に合ったので良かったです。インパクトもあったようです。

最初からいきなり関数型プログラミングばりばりでいくのではなく、初心者がもっととっつきやすいように、(害のない範囲で)手続き型プログラミングも認めていこうよ、という話だと理解しました。特に、メソッドローカルに閉じている限り、var/whileやミュータブルコレクションを使って構わないという主張には、私も賛成するところです。最近のScala界隈は、too functionalな面があると感じていて、自分も、こういう取り組みをしていけたらと思わせられる良い発表でした。

フィボナッチサービスという例題を使って、jvisualvmでボトルネックを発見する方法を解説した発表です。jvisualvmについては既に使ったことがあり、新しみはあまりなかったのですが、発表のわかりやすさという点で学ぶべきところが多かったです。

scalafmtを使ったことは(ほぼ)なかったので、そのアグレッシヴさ、というか、設定項目の多さに仰天しました。高橋さんとしては、どっちも十分使えるので好みで良いということでしたが、個人的には、sbtでコンパイル時に自動フォーマットできるscalariformの方が好みかなというところです。また、Scala Style Guideについて

  • 標準化されていない
  • スタイルが未定義な部分がある
  • 矛盾した部分がある

という指摘があり、なるほどと思いました。scalariformは手書きのパーザを使っているらしいという情報を得たので、ちょっとパーザみてみようかと思いました(構文解析屋としての興味)。

なお、私は、最初に書いたように「プレースホルダ構文完全解説」という発表を行いました。発表を聴きに来てくださった方はだいたい30~40名くら居たのですが、正直、うまく説明できたのかあまり自信がありません。元々、テーマ的にマニアックである上に、理解するにはコンパイラやパーザの知識が必要になる、という点でかなり聴く人を選ぶものだったので、全員に伝わるように発表するのが難しかったのかもしれません。一部の方には面白かったと言っていただけたのは嬉しかったです。

新幹線であらかじめ帰りの指定席を予約していたこともあり、懇親会には参加せずに離脱。後ほどTwitterを眺めてみると懇親会の楽しそうな様子が伝わってきて、ちょっぴり後悔しました。

主催者の、Scala関西ユーザーグループの方々、楽しい一日でした。ありがとうございました!来年は、もうちょっと万人受けするネタを考えようと思っています…

プレースホルダ構文と文字列補間を組み合わせた時の挙動についての考察

久しぶりの日記の更新です。今回のテーマは、Scalaにおける、Placeholder syntax for anonymous function(以後、プレースホルダ構文)とString Interpolation(文字列補間)を組み合わせた時にどのような挙動であるべきか、また、現状の挙動が妥当かについて考察する記事です。

きっかけは、本日のきの子さんのツイートでした。

文法上はできるはずだけど、どういうケースなのだろう、と疑問に思ったのですが、吉田さんのツイート

のリンク先をみて、補間文字列の中に現れるプレースホルダが、その外側と結びつかないことが問題ということのようでした。

ただ、リンク先では

It can probably be made to work; pull requests welcome.

と軽く言っちゃってますが、これ、プレースホルダ構文について以前詳細に調べたことのある身としては、なんか許しちゃ駄目なケースではないかと瞬間的に思いました。ただ、文字列補間の詳細な仕様を参照したことがなく、自信があったわけではないので、両方の仕様を突き合わせたり、サンプルの入力を考えた時に、プレースホルダが補間文字列の外側と結びついて大丈夫なのか考えてみることにしました。

まず、プレースホルダ構文についてですが、Scala Language Specificationの6.23.5に仕様が規定されています。ただ、これが意味するところは結構ややこしいので、自分が以前書いた解説記事をベースに話を勧めます(当該記事も、仕様書を元に説明したものなので、改めて解説する手間をはしょっただけです)。

文字列補間については、SIP-11 - String Interpolationに仕様が定義されているので、これを元にして考察します。

解説記事にも書いているのですが、プレースホルダ構文は構文解析時に作用する要素であるという点が非常に特異な代物で、文法定義上のカテゴリという概念を参照して定義されています。一方、文字列補間も構文解析時に主に処理が行われるもので、両者を組み合わせた場合のことをテキトーに考えると、おかしな結果になることは割とすぐに想像がつきます。

たとえば、GitHubのIssueに上がっている例では、

val f : Int => String = s"The there are ${_} apples in the bowl"

val f: Int => String = n => s"The there are $n apples in the bowl"

と解釈されて欲しい、という事が書いてあります。ぱっと見で、文字列リテラルっぽいものの型が関数の型になるという結果で、かなり直観的ではないのですが、さて、これは許されるべきなのでしょうか。まあ、想像だけで考えても仕方ないので、String Interpolationの方の仕様を見てみます。

関係する部分を抜粋すると、

A processed string literal of either of the forms

 id ”text0${ expr1 }text1 … ${ exprn }textn”
    id ”””text0${ expr1 }text1 … ${ exprn }textn”””

where each texti is a possibly empty string not containing dollar sign escapes, is equivalent to:

StringContext(”””text0”””, …, ”””textn”””).id(expr1, …, exprn)

と書かれています。これは、processed string literal(fとかsとかがプレフィックスについた文字列リテラルのこと。補間文字列リテラルと呼びます)が、StringContextオブジェクトのメソッド呼び出しに変換され、式の部分は引数として渡されるということを言っています。このような仕組みになっているのは、文字列補間をユーザ定義可能にするためなのですが、それはさておき、この説明を読む限り、補間文字列リテラルの型は、プレフィックスと同じ名前のメソッド呼び出しによって決定されるのであって、中にプレースホルダがあったら、 Int => String に型が変わるというのは、現状の文字列補間の仕様と衝突します。要求を通すなら、少なくとも、現状の仕様を、StringContext を使わない形に解釈され得るという風に更新する必要がありそうです。

じゃあ、文字列補間の仕様を更新することを前提にするなら、OKか。駄目とは言い切れないものの、結構微妙なところがあります。まず、補間文字列の構文カテゴリは SimpleExpr1 だと定められており、補間文字列中の ${...}BlockExpr であると定められています。プレースホルダ構文の仕様としては、構文カテゴリ Expr_ を含み得るという形で定義されており、かつ、SimpleExpr1BlockExprExpr に昇格可能なので、補間文字列中の式が外側の式と結びつくのは、構文カテゴリの点で言えば、既存の仕様と矛盾していないとは言えそうです。

ただ、矛盾していないからいいかというと…次の例を考えてみます。

List("A", "B", "C").map(s"alphabet = ${_}")

これが、

List("A", "B", "C").map(a => StringContext("alphabet = ", "").s(a))

と変換されて欲しいとします(し、実際の要求としても、こういうのがありそうです)。この場合、プレースホルダ構文が処理されて、その結果として、補間文字列の式が a => StringContext(...).s(a) になる、という解釈になるでしょう。

別の例として、

(List(1, 2), List(3, 4)).zipped.map(s"${_ + _}")

がどう変換されるべきかを考えてみます。プレースホルダ構文の仕様の方はいじらないなら、これは

(List(1, 2), List(3, 4)).zipped.map((x, y) => StringContext("", "").s(x + y))

に変換されるべきです…ほんとに?実は、ちょっとごまかしが入っていて、文字列補間による式変形と、プレースホルダ構文による式変形がなんとなくうまくはまったかのように書いていますが、十分に挙動を説明しきれていません。

正確に仕様を書こうとするなら、文字列補間による式変形の順番とプレースホルダ構文による式変形の順番を規定する必要があります。しかし、困ったことに、文字列補間は式の(構文カテゴリ上の)形を変えてしまうので、どちらを先に適用するかで最終結果が変わる可能性があります。

上記の例で言えば、文字列補間→プレースホルダ構文という順番で処理されると考えるのなら、

(List(1, 2), List(3, 4)).zipped.map(StringContext("", "").s(_ + _))

その後に

(List(1, 2), List(3, 4)).zipped.map(StringContext("", "").s((x, y) => x + y))

となります。逆の順番なら、

(List(1, 2), List(3, 4)).zipped.map((x, y) => s"${x + y}")

その後に

(List(1, 2), List(3, 4)).zipped.map((x, y) => StringContext("", "").s(x + y))

となります。このケースでは、後者、つまり、プレースホルダ構文→文字列補間、の順番に変形してくれた方が嬉しいでしょう。しかし、順番を考えないとうまく動くかどうか判断できないのは仕様としてはあんまりだと思うのです。

プレースホルダ構文が補間文字列リテラルの外に影響を与えない、という現状の仕様であれば、(おそらく)処理順序による影響は出ません。自分としては、構文上の仕様に(プレースホルダ構文だけでもめんどうなのに)これ以上ややこしいものを放り込んでほしくないので、これはちょっと…と思うところです。

長くなりましたが、結論としては、

  • 現状のプレースホルダ構文と文字列補間の仕様の整合性は取れている。要求に従うと現状の仕様に不整合が出る
    • 仕様変更が必要
  • プレースホルダ構文が補間文字列リテラルの外側に影響を与える仕様変更を行うには、両者の処理順序を明記する必要がある
    • 複雑であり、わかりにくい挙動
  • 結論としては、現状の仕様のままが妥当そう

といったところになります。

結構ややこしい問題で、自分の考察になんか穴がある可能性はあるので、もしあれば指摘していただければと思います。

Klassic言語開発日誌 〜列多相(row polymorphism)〜

最近、Klassic言語をちょくちょく更新しているのですが、半月くらい前にオブジェクトに関係する型推論の仕組みを入れました。知っている人は知っていると思いますが、OCamlとかにあるアレです。たとえば、

def distance(p, e) = {
  // abs: Double => Double
  // double: Int => Double
  val dx = abs(double(p.x - e.x))
  val dy = abs(double(p.y - e.y))
  sqrt(dx * dx + dy * dy) // sqrt: Double => Double
}

こういうKlassicプログラムがあったとします。これは、 Player ( p )とEnemy ( e )が引数に与えられたとき、それぞれが xy というメンバがあると仮定して、PlayerとEnemyの間の距離を計算する、というものです。

この関数の型を推論させてみると、以下のように推論されます。

distance : (
  record{ x: Int; y: Int; ...},
  record{ x: Int; y: Int; ...},
) => Double

算術演算子を適用しようとした場合に、他にいい候補がないときにデフォルトで Int を対象として選択するのは、現状のイケてない点ではありますが(型クラスを入れるのがいいのだろうか…)、とりあえず意図通りに推論されています。

まだ、メソッドの型推論を真面目にやっていないので、来月中にはその辺をきちっと作り込みたいところですが、ようやくオブジェクト指向言語を名乗ってもいいような感じになってきました。

ところで、row polymorphismってなんで行多相じゃなくて列多相って訳が定訳(?)なのでしょうか。どなたかご存知の方が居れば情報をいただきたく。

「構文解析ハンズオン」を開催しました(2017/07/01)

今まで、あまり見たことがない(一般エンジニア向け)勉強会で、かつ、これを学ぶことは実用上とても意味があるテーマの一つに「構文解析」があります*1

たとえば、Webアプリケーションにおいて、ユーザの入力に大して何らかの構文的制約をつけてバリデーションをする機会は多いですが、正規表現による簡単なチェック(あるいは、滅茶苦茶複雑な正規表現を作って色々な入力のバリエーションに対応する)や、よくわからない緩いバリデーションで済まされることは多いように感じます(自分の開発経験というより、Webアプリケーションのフォームのバリデーションの仕方などを見ての感想です)。

正規表現によるバリデーションで十分なことも多いので、それをむやみに否定するものではありませんが、入力が(原理的に)正規表現でバリデーションできない場合や、可能だが正規表現が爆発するケースだと、正規表現が不適当なこともしばしばです。

個人的には(文脈自由な)構文解析ができる技能があれば、もっと精度の高いバリデーションができるのに、と思っていたこともあり、構文解析の基本を学ぶ勉強会を開催してみることにしたのでした。

構文解析ハンズオン - connpass

というわけで、昨日に開催したのが、↑の勉強会です。ハンズオン形式で、1桁整数の構文解析JSON構文解析を学ぶ、というものです。

大学における構文解析の授業では、手書き構文解析の方法として、LL(1)のようなものを再帰下降パーザで模倣することが非常に一般的ですが、昨日の勉強会では、時間的制約( 全部で6時間程度しかない)や、自分のPEG推しという趣味もあって、PEGベースの再帰下降パーザを学んでもらう構成にしました。

参加者層は本当に構文解析初学者が多いので、PEGという言葉をだすと混乱するだろうと思われたため、そういう言葉を出さずに、PEG的な発想をそのまま教えるというアプローチを取りました。結果としては、短い時間でJSONのような再帰的構造を持つ言語の構文解析までたどりついてもらうには、それなりに有効なアプローチだったと思います。

反省点については

  • 資料を準備する時間が十分でなかったため、当日、大量のアドリブを入れて説明をする必要があった
  • 自分の構文解析器をテストするように、用意したプログラムを修正してもらうというやり方は遠回り過ぎた
  • 参加者層に対する要求である「Javaが普通に書ける」が曖昧過ぎて、参加条件を満たさない方も参加してしまった
    • それらの方に対する苦情というより、それだとハンズオンで構文解析を学んでもらうことが難しいので、結果的に申し訳ないことになってしまったなという意図です
  • 資料の細かいミスが結構あった
  • , etc.

と色々あるのですが、構文解析の基本をおさえてもらうという目的はある程度達成できたかなと思っています。

興味深かった(一部の方の)反応として、わかったか自信がないけど、心を無にすれば書けたという趣旨の発言が見られたことです。BNFからPEGベース手書きパーザへの(ほぼ)機械的な変換方法を提示するというやり方だったからだと思うのですが、PEGベースのパーザの単純さ、理解の容易さがそれに貢献したのではないかなと思います。

構文解析をテーマにした勉強会については、当分はしないと思いますが、その他の、「(情報系の)学部だと普通にならうけど、自習がやや難しい」系のテーマについて、積極的に教える機会を作っていきたいなと思っています。

*1:自然言語構文解析を主に指します

技術イベント「Understanding Scala」を開催しました(6/10)

Understanding Scala - connpass

昨日、表題の技術イベントを自分主催で行いました。なんでこんなイベントをやろうと思ったかというと「皆、Scalaを難しくめんどくさい方法で学んでるのでは?」という疑問が自分の中であって、その原因として、サンプルプログラムの集合を通して、ボトムアップになんとなくイメージで 全体像を作りあげてるのではという思いがありました。

そのようなアプローチに対して懐疑的な自分としては、このイベントでは(厳密にはやってませんが)どちらかというとトップダウン的アプローチでプログラミング言語について理解してもらおうと思い(もちろん、例は大切なので必要に応じて詳細に下りるのは忘れませんでしたが)、5つの発表の全てを全部自分でやりました。さすがに、5時間程度しゃべりっぱなしというのは疲れましたが、おかげさまで(?)、色々な疑問が解決したとか、メソッドと関数の違いがわかったとか、狙い通りの成果を持ち帰っていただけた方も多いようで、報われた気持ちになりました。

なお、発表資料は以下にあります:

後ろ2つの発表については、Scala特有の事項が多く出てきますが、最初の3つの発表は、学んだことを他の言語を学ぶ際にも多少は応用できるように内容を考えていたように思います。

自分としては、構文、型システム(がある場合)、意味論に分けて考えるのは、他のプログラミング言語を学ぶのに役立つと思うので、そういうアプローチでプログラミング言語を理解しようとする人がもっと増えてくれるといいなと思っています。