Apache Sparkを使うためにScalaを覚え始めて半月ほどですが、いきなりTypeTagなるものを知るはめになるとは・・・。
1. 何がしたかった?
Sparkプログラミングの基本のデータ型であるところのDatasetを引数に取る関数を書きたかった。
DatasetはDataset[T]として定義されているもので使うときにはTに具象クラスを指定する必要がある。
で、その件の関数は、Tにいろいろな型を取りたかったので、ジェネリクスを使うことにした。
def someMethod[T](arg1: String, arg2: Dataset[T]) = {
import arg2.sparkSession.implicits._
arg2.map(ほにゃらら).reduce(ごにょごにょ)
}
2. 何がダメだった?
上記をコンパイルすると、以下の通りのエラーとなる。1
Unable to find encoder for type stored in a Dataset. Primitive types (Int, String, etc) and Product types (case classes) are supported by importing spark.implicits._ Support for serializing other types will be added in future releases.
補足を交えつつ意訳すると、
- Sparkの分散処理の仕組み上、Dataset(RDDもですが)は別のPCなりサーバなりにコピーする必要があり、それはネットワークを流れる関係上、シリアライズする処理(エンコード)が必要。
- Dataset[T]のTがプリミティブ型やProduct型だったら暗黙のエンコーダを用意してるんだがね、ウケケ
ということらしい。
いや待った待った。Tにはcase classで定義した型を使うからつまりProduct型なんすよ?
と半日ほど悩んで、ハッと気が付く。
ああ、コンパイラはTにProduct型が指定されるなんてこと、知りっこねえわな・・・。
3. 解決方法
Scala経験の浅い自分ですから、なんて検索していいのかわからなかったんですが、なんとかして答えを見つけました。同じようなエラーの人がいましたよっと。
ふむふむ。
[T <: Product]
こう書くと、TはProductのサブクラスである(でないとダメ)ということをコンパイラに教えてあげられるわけですね。
これを、型パラメータの上限境界 2というらしいです。
あ、でもこの人はまだこれじゃ解決してないよって、質問を投げかけてるわけですよね。自分も試してダメでした。
答えは
[T <: Product: TypeTag]
こう書きなよ、ってことらしいです。
????????どゆこと?
まあとにかく、回答を読む限り、
ソース読んでみたら、Sparkの暗黙変換関数はT(の具象クラス)のTypeTagをご所望されているので、それを定義してやる必要があるよ
ということらしいです。
いや全然わかんないし・・・。
でもコンパイルは通りました!めでたい!
4. 何やってるの?
しかしこれは何をやっているのだろう。
これはもう公式ドキュメントを当ってみるしか・・・。
https://docs.scala-lang.org/ja/overviews/reflection/typetags-manifests.html
曰く。
context bound [T: TypeTag] からコンパイラは TypeTag[T] 型の暗黙のパラメータを生成して前節の暗黙のパラメータを使った用例のようにメソッドを書き換える。
ふむふむ、なるほど。まったくわからん。
後半はわかります。その説明の前の例、
def paramInfo[T](x: T)(implicit tag: TypeTag[T])
を
def paramInfo[T: TypeTag](x: T)
こう書いていいよー、って言ってるわけですよね。シンタックスシュガー的なものですね。
で、context boundって?ググるとやっぱりシンタックスシュガーだって書いてあるところが多いですね。
ということで、正規の書き方としては暗黙のパラメータ形式でTypeTag型の何かを渡したいんだな、ということは了解。
で、で、で、TypeTagって何?TypeTagはなんで必要とされているの?必要とされてることを「ソース読んでみたらそう書いてあったよー」ってのは正しい解決法への道のりだったの?(ドキュメントに書いてないの?)
とか色々疑問が沸くわけです。
TypeTagって何?TypeTagは何で必要とされているの?
これは先の公式ドキュメントに書いてある通りですかね。
他の JVM言語同様に、Scala の型はコンパイル時に消去 (erase) される。 これは、何らかのインスタンスのランタイム型をインスペクトしてもコンパイル時に Scala コンパイラが持つ型情報を全ては入手できない可能性があることを意味する。
今回の例であれば、TがProductのサブクラスであれば、エンコーダが用意されているけど、TがProductのサブクラスかどうかなんてことは(TypeTagがないと)コンパイルしちゃうとわからなくなっちゃう。だからTがProductのサブクラスであることをコンパイルしても覚えておきましょうね、それがTypeTagですよ。ということだと理解した。
ソース読むの?ドキュメントは?
これは調べた限り不明。暗黙の了解、的なことなんでしょうか。もう暗黙(implicit)は勘弁です。。。
そもそもこのエラーは誰が吐いてるの?
まあ、コンパイラが吐いてるわけなんですが、Sparkに特化してるわけではないScalaのコンパイラが言うにしては、やたらと具体的なメッセージ。気になる。
(プリミティブとProductは)spark.implicitsをインポートすればOKだの、それ以外は将来のリリースで対応する予定、だのSparkに忖度しすぎだろ!
もちろん、scalacがSparkに忖度しているわけはないから、Sparkが用意したメッセージのはずで、ならばSparkのソースの中に定義されているはず。
で、ありました。
org.apache.spark.sql.Encoderのソース(github)
の中に
@implicitNotFound("Unable to find encoder for type ${T}. (以下略)")
なる定義が。なるほどアノテーションてやつね。
流れを整理すると
こんな感じですかねえ。
- Scalaコンパイラが、Encoderが必要な場面にでくわす。
- 暗黙のEncoderを探す
- Dataset[Product]のEncoderならあるんだけど、どこの馬の骨か分からないT型を使ったDataset[T]に食わせてやるEncoderはねえ!
- Dataset[T <: Product]って書いても、「そうねえ、言いたいことは分かるんだけど、あなたコンパイルしたらProductだったこと忘れてしまうでしょ?だからこのEncoderはなかったことにしましょう?」
- つまり、Encoderは見つからない。
- ここでアノテーションがなければscalacのデフォルトのエラーメッセージが出るはずだが、アノテーションがあるためにSparkの内情を知り尽くしたメッセージが出る。
- アノテーションまで書いてヒントを出してるんだから、TypeTagで解決することはお察し(マジか)。
5. まとめ
そんなわけで、黒魔術的手法を駆使してJavaソースの冗長さを殲滅せんとするScala、私は好きですよ。
(ただし最近のJavaの仕様には詳しくないのでJavaもJavaなりに進化を遂げてるもんだと思いますが)
どんなまとめだ。