ろじかるんるんものがたり

病人が特に何も書かない。無駄だからだ。

Scala 勉強会第 165 回に参加しました

懇親会に行くたびに、生活保護受給者は辛いなという気分になっています。

お題が Hot Topic だったのですが今 Hot なのはそりゃ Scala Matsuri でしょうということで、xuwei_k さんと OE が海外勢の紹介したり。あと Hot(になりそうな)Topic ということで、2016 年の Scala という話をちょっとしたりもしました。我ながら無理やりすぎる。

 

ところで Scala 勉強会には、自己紹介の後に「Scala 入門タイム」というのがあります。

経緯としては、3 年程前に急に初心者さんが増え始めた時期がありました。それまでは初心者なんてあまりいなかったので、なんとかしないといけない、ということになり、twitter が公開していた Scala School を読んだりしてました。

がまあ個人的にはこれはうまくなかったと感じており、やめましょうという発表をしたりしました、資料。以来、入門タイムが始まると、大体毎回進行をしてくれている qtamaki さんが「質問ある方ー」みたいな感じで聞いて、各位がその場で答える、みたいな緩い感じでやっています。

病気の療養に専念していた時期は勉強会に参加していませんでしたが、ここ最近の数回行った限りでは昔と変わりなく続いていたようで、今の形にしてよかったなーと個人的に思ってます。たまに回答者が間違えてたり(私です)もしますが、それも元々狙いの一つだったのでいいでしょう。すいませんでした…そして今回も間違えてました。後述。すいませんすいません…

 

というわけで、165 回では二つの質問がありました。

 

一つ目。「implicit conversions ってどう定義するのがいいんですか」という質問。

例えば以下のような。

scala> implicit def `implicit conversions example`(n: Int) = new { def str = n.toString() }

warning: there was one feature warning; re-run with -feature for details

implicit$u0020conversions$u0020example: (n: Int)AnyRef{def str: String}

まず警告について。これは -feature オプションつけてやりなおせと叱られてますが REPL なら :warnings コマンドで最後の警告の詳細が出力できます。

scala> :warnings
<console>:10: warning: implicit conversion method implicit conversions example should be enabled
by making the implicit value scala.language.implicitConversions visible.
This can be achieved by adding the import clause 'import scala.language.implicitConversions'
or by setting the compiler option -language:implicitConversions.
See the Scala docs for value scala.language.implicitConversions for a discussion
why the feature should be explicitly enabled.
implicit def `implicit conversions example`(n: Int) = new { def str = n.toString() }

import するかコンパイルオプションつけろ、詳細知りたければドキュメント見ろと言われます。

これは SIP-18 (Scala Improvement Process) で提案された機能です。Modularizing Language Features といいます。一部の言語機能(強力な物や、実験的なもの)について、ライブラリの API を import するように、言語機能を import する形で言語自体をより modular にしよう、という提案です。Scala は modular であることに一定のこだわりがあります(特に Odersky 先生が)。

implicit conversions は pimp my library とかできて便利で強力なのですが、割と危険なことやおかしなことも出来ちゃうので、分かってる人が分かった上で使うべきでしょう、ということだと思います。

そんなわけで import してやり直す。

scala> import scala.language.implicitConversions
import scala.language.implicitConversions

scala> implicit def `implicit conversions example`(n: Int) = new { def str = n.toString }
implicit$u0020conversions$u0020example: (n: Int)AnyRef{def str: String}

scala> 1.str
warning: there was one feature warning; re-run with -feature for details
res2: String = 1

また叱られました。

scala> :warnings
<console>:25: warning: reflective access of structural type member method str should be enabled
by making the implicit value scala.language.reflectiveCalls visible.
This can be achieved by adding the import clause 'import scala.language.reflectiveCalls'
or by setting the compiler option -language:reflectiveCalls.
See the Scala docs for value scala.language.reflectiveCalls for a discussion
why the feature should be explicitly enabled.
1.str

さっきと同じですね。しかし別の言語機能について警告されているようです。

これは SIP-18 でいうところの "structural type" (言語仕様的には Compound Types が正確な呼称ですが…)に対するメソッド呼び出しは、分割コンパイルの実現のために Java の reflection API を用いたコードに変換されるため(細かいことをいうと、メソッドキャッシュとかもするのですが省略)速度的に不利になりますよ、気をつけましょうね、ということで叱られているわけです。

implicit conversions を定義するときに、クラス定義したり型シグネチャ書いたりをサボる人が一定数いるわけですが、これは行儀がよいとは言えません。今回のような単純な例なら Implicit Classes を使いましょう。

scala> implicit class `implicit conversions example`(n: Int) { def str = n.toString }
defined class implicit$u0020conversions$u0020example

書いてて気が付いてしまったのですが SLS(Scala Language Specification) に Implicit Classes についての記述が全然ない。それをいったら Modularizing Language Features についても記述がない気がするけど。

 

二つ目。ScalaFX で、呼び出せないメソッドがあるという話。

具体的には ObservableMap#remove が呼び出せない。ObservableMap は JavaFX と ScalaFX の両方で同名のクラスが定義されており、暗黙変換が定義されている。さて何故呼び出せないのか。

これはメソッドが同じ名前で定義されている上に Java 側がパラメタとして Object 型の値をとるものだから、暗黙変換を起こそうにも起こせない、という話です。悲しいですね。

勉強会で、型注釈つければ多分いけるんじゃないかなーという話をしたんですが、また嘘つきました。無理でした。

こういうのはいけます。

scala> def f(a: Int) = 1
f: (a: Int)Int

scala> def f(a: Int) = "hoge"
f: (a: Int)String

scala> val s:String = f(1)
s: String = hoge

ここから暫くコンパイラ内部実装の話なので読み飛ばしていいです。

上のコードが通るのは、typer フェーズで Assign に対する型付けを typedAssign メソッドで行う時に、lhs の型をヒントに rhs の型を解決しようとするからです。以下ではこのヒントを pt と呼びます。

しかし、Apply に対する型付けを行う typedApply ではpt 生かされません。より正確には、typedApply 内部で呼び出される normalTypedApply で型付けに失敗した後、暗黙変換も考慮する tryTypedApply までいって初めて、暗黙変換の候補を探索するわけですが、この際 pt は考慮されません。よって型がつけられないよ、と型エラーだと言われるわけです。

ちなみにこれは仕様にもきちんと書いてあって、メソッド解決に失敗した場合の暗黙変換は、pt を考慮しません。

ということでまた勉強会で嘘をついてしまいました、すいませんでした…いやだって前述のコード通るんだもん、通ると思うじゃないですか…暗黙変換は pt 考慮しないんだなあ…関数として一旦取り出すのも無理です、MethodType から FunctionType への変換が先に必要だからかな…Scala 難しいですね…

 

まとめると、暗黙変換定義する際には、同名かつシグネチャの近いメソッドは避けたほうがいい、という話です。何かしらのラッパーを提供する場合、標準の JavaConversions みたいに提供するのが個人的にはおすすめです。

 

あー長い!終わり!