Scalaでも演算子のオーバーロードをしたい
はじめに
Scalaの演算子オーバーロードについてサクッと確認してみます。
オーバーロード
まぁ、とりあえず言語仕様を見ておけば安心だよねといった感じです。困ったら公式ドキュメントを読む。日本書紀にもそう書いてあります。
case class AltInt(val value: Int) { def +(x: AltInt) = plus(x) def plus(x: AltInt) = AltInt(value + x.value) def unary_~ = AltInt(value * -1) def +:(x: Int) = AltInt(value + x) def -:(x: AltInt) = { println(s"$value - ${x.value} = ${value - x.value}"); AltInt(value - x.value) } def *(x: AltInt) = AltInt(x.value * value) def ! = println(value) }
中置演算子
Scalaでは引数を一つ取るメソッドはすべて中置演算子として扱えます。
逆に+
のような演算子も普通*1のメソッドのように呼び出せます。
val a = AltInt(10) val b = AltInt(20) println(a.plus(b)) println(a plus b) println(a.+(b)) println(a + b)
なお、優先順位はメソッドの先頭の文字によって規定されるようです。
先頭の文字 | 優先度 |
---|---|
(all letters) | 低い |
| | |
^ | |
& | |
= ! | |
< > | |
: | |
+ - | |
* / % | |
(all other special characters) | 高い |
val a = AltInt(10) val b = AltInt(20) println(a * a + b) // 120
また、メソッドが:
で終わる場合は引数とレシーバの位置関係が逆転します。
println(1 +: a)
また、通常演算子は左結合ですがこの場合は右結合になります。
println(b -: a -: a)
10 - 10 = 0 0 - 20 = -20 AltInt(-20)
後置演算子
中置演算子と同じように引数を取らないメソッドは後置演算子として呼び出せます。
println(a!) println(a.!)
前置演算子
前置演算子はunary_記号
で定義できますが、記号に入ることができるのは+
・-
・!
・~
に限られます。
println(~a) println(a.unary_~)
おわりに
素直でいいですね。
*1:普通?普通ってなんだ?
implicitパラメータとは何ぞや
はじめに
PlayでコントローラからビューからFuture[T].map
からお前らimplicit
パラメータ好きすぎだろってくらい多用されています。これはゆるふわっと使っていると後でヤバい目に遭うなと思ったので軽く調べてみました。
ゆるふわ愛されimplicit
言語仕様のimplicitsセクションを見ながら確認します。困ったときは公式ドキュメントを読む。古事記にもそう書いてあります。
とりあえず以下が一番シンプルな形です。
object Hello { def main(args: Array[String]): Unit = { implicit val hoge = "jyuch" greet("Hello") } def greet(message: String)(implicit who: String): Unit = { println(message + " " + who) } }
Scalaコンパイラはimplicit
パラメータが明示的に与えられていないときはメソッドの呼び出し箇所からプレフィックス無しでアクセス可能なimplicit
付き識別子を探し出してパラメータを適用します。
Scalaコンパイラ頑張りすぎてない?大丈夫?過労死しない?顔色悪いんじゃない?
また、プレフィックスなしでアクセス可能なら検索対象になりうるということは、何でもかんでもワイルドカードでインポートすると誤爆する可能性もあるってことですよね。
当たり前と言えば当たり前ですが、候補となるパラメータが複数ある場合はどちらを使えばいいか判断ができないためコンパイルエラーとなります。
def main(args: Array[String]): Unit = { implicit val hoge = "jyuch" implicit val fuga = "hoge" // Error:(8, 10) ambiguous implicit values: // both value fuga of type String // and value hoge of type String // match expected type String // greet("Hello") greet("Hello") }
こちらも当たり前と言えば当たり前ですが、トレイトの実装クラスや抽象クラスの実装クラスも適用します。
object Hello { def main(args: Array[String]): Unit = { implicit val fuga = new Fuga() greet("Hello") } def greet(message: String)(implicit who: Hoge): Unit = { println(message + " " + who.who) } } trait Hoge { val who = "hoge" } class Fuga extends Hoge { override val who = "jyuch" }
また、シングルトンオブジェクトでも出来るようです。(これは意味の無い例ですが)
object Hello { implicit object Hoge { val who = "jyuch" } def main(args: Array[String]): Unit = { greet("Hello") } def greet(message: String)(implicit who: Hoge.type): Unit = { println(message + " " + who.who) } }
また、パターンとして
- compound type(
hoge: Hoge with Fuga
みたいなアレ) - parameterized type(
S[T1, T2...Tn]
みたいなアレ) - type projection S#U(ネストクラスをいい感じにするアレ)
- 型エイリアス
implicit val aaa: Hoge => Fuga = { x: Hoge => Fuga(x.name) }
でdef piyo(implicit a: Hoge => Fuga)
ってなるアレ(名前を知らない)
も大丈夫なようです。*1
で、一番気になるのが暗黙の型変換と暗黙のパラメータを組み合わせて適用可能なのかといったところです。これが可能ならば暗黒プログラミングが捗る可読性が下がってつらいですね。
object Hello { implicit def hoge2Fuga(hoge: Hoge): Fuga = hoge match { case Hoge(x) => Fuga(x) } def main(args: Array[String]): Unit = { implicit val name = Hoge("jyuch") greet("Hello") } def greet(message: String)(implicit who: Fuga): Unit = { println(message + " " + who.name) } def piyo(implicit a: Hoge => Fuga) {} } case class Hoge(name: String) case class Fuga(name: String)
Error:(14, 10) could not find implicit value for parameter who: example.Fuga greet("Hello") Error:(14, 10) not enough arguments for method greet: (implicit who: example.Fuga)Unit. Unspecified value parameter who. greet("Hello")
暗黙の型変換をぶちかましつつ暗黙の引数に突っ込むまではしないようです。
おわってない
と、ここまでで仕様書のImplicit Parametersセクションの半分です。
残り半分くらいでネストされた場合のimplicit
パラメータの挙動について述べていますが、力尽きました。
おわりに
Scalaむずかしい
*1:2.12の言語仕様ではもう一つ箇条書きの項目があるのですが、何を言っているのか弊社には全然わかりませんでした
ScalaでもリフレクションでtoStringしたい
はじめに
Scalaを始めたのでとりあえずリフレクションでtoStringしてみました。 弊社にとってリフレクションでtoStringするのはもはや挨拶のようなものです。
環境
Scalaのバージョンは2.12.3です。
真のジェネリック
当たり前かどうかは分かりませんが、JVMの型パラメータはコンパイル時に消去されてしまい実行時に取れません。 この辺の問題は『真のジェネリック問題』として2012年をピークに激しく議論されていたようですが、流れ弾はいやなのでとりあえずスルーします。
当然JVMの民*1であるScalaもこの制約を受けます。その為、CLRの民であるVBのように
Sub Hoge(Of T)(obj As T) Dim t As Type = GetType(T) End Sub
のようにGetType(T)
で一発で型情報を取得してアビャーするわけにはいかないようです。
そこで、Scalaはコンテキストバインドという形で型に関する情報をコンパイル時に必要なメソッド等に引き渡すコードを生成し、そこで型情報をやり取りするようです。
仮に
def reflectToString[T: TypeTag : ClassTag](value: T): Unit
のようなシグネチャを持つメソッドは以下のようにコンパイルされます。
> javap -private .\Hello.class Compiled from "Hello.scala" public final class example.Hello { public static <T> void reflectToString(T, scala.reflect.api.TypeTags$TypeTag<T>, scala.reflect.ClassTag<T>); public static void main(java.lang.String[]); }
本来の引数の後ろに型情報に関するパラメータを追加し、コンパイラがコンパイル時に型情報を暗黙的に追加するようです。
package example import scala.reflect._ import scala.reflect.runtime.universe._ class Hoge(val name: String) class Person(val name: String, val age: Int) object Hello { def main(args: Array[String]): Unit = { val p = new Person("Hoge", 10) reflectToString(p) println() val h = new Hoge("HogeHoge") reflectToString(h) } def reflectToString[T: TypeTag : ClassTag](value: T): Unit = { val fs: Iterable[TermSymbol] = typeOf[T] .decls .filter(_.isTerm) .filter(!_.isMethod) .map(_.asTerm) val rm: Mirror = typeTag[T].mirror val im: InstanceMirror = rm.reflect(value) for (it <- fs) { val fm: FieldMirror = im.reflectField(it) println(s"${it.name}= ${fm.get}") } } }
name = Hoge age = 10 name = HogeHoge
TypeTag
とってTermSymbol
とってRuntimeMirror
とってInstanceMirror
とってFieldMirror
とってget
するのは.NETのリフレクションに慣れた身からするとなんか複雑に感じますが、まぁしゃーない。
おわり