追記
間違っていないことを示していただけました →
「Scalaに存在演算子を求めるのは間違っているだろうか」の解答例 - scalaとか・・・
さらにLensでも →
「Scalaに存在演算子を求めるのは間違っているだろうか」をLens/Prismで解いてみる - 独学大学情報学部
(間違っていないけど、そういう抽象化が適切とは言っていない)
精進せねば・・(´・_・`)
追記終わり。
あなたはそこにいますか
CoffeeScriptには存在演算子?.
があります。
これは、JSONのようなネストした構造で、かつ値が途中でnullになってるかもしれないけど一番内側の値が欲しい! ときに便利な仕組みです。
コードとしては
obj = { e : { d : { c: { b: { a: { value: 1 } } } } } } # 存在演算子を使ってvalueにアクセス! obj?.e?.d?.c?.b?.a.value
のように書くと以下のようにコンパイルされます。
var obj, ref, ref1, ref2, ref3; obj = { e: { d: { c: { b: { a: { value: 1 } } } } } }; if (obj != null) { if ((ref = obj.e) != null) { if ((ref1 = ref.d) != null) { if ((ref2 = ref1.c) != null) { if ((ref3 = ref2.b) != null) { ref3.a.value; } } } } }
コンパイル後のコードを見てみると、edcbaに対してひたすらnullチェックを行っていることが分かります。
Scalaでの存在
まず本編通してのbuild.sbtを貼っておきます。
name := "OptionValueAccessor" version := "1.0" scalaVersion := "2.11.6" libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
さて、Scalaで同じことをやるとしたらどうでしょうか?
まず、以下の様な状況を想定します。
case class A(value: Int) case class B(value: Option[A]) case class C(value: B) case class D(value: Option[C]) case class E(value: D) val obj = Some(E(D(Some(C(B(Some(A(1))))))))
ある値は必須だけど、別のある値は必須ではない。そういう値がネストしているケースです。*1
Scalaでこういうオブジェクトにアクセスしたいときは普通は以下のようにするのではないでしょうか。
for { edcba <- obj dcba <- edcba.value cba <- dcba.value ba <- cba.value a <- ba.value } yield a.value
せやな。と思ったそこのあなた!これ動かないんですよ!edcba = E(D(..))
となってるのでflatMapが使えないんですね。
正しくはこうです。
for { edcba <- obj dcba = edcba.value cba <- dcba.value ba = cba.value a <- ba.value } yield a.value
えーこれめんどくさ過ぎでしょ(◞‸◟)
もとのcase classのvalueを全部Optionにすれば良いんですが、それだと「これは必須要素でこれはオプション要素で・・・」といった知識を型で表現することが出来なくなってしまいます、 あと(個人的な感覚ですが)、CoffeeScriptほどシンプルには書けていないと思います。
ということで、いっちょアクセスできるものを作ろうと思い立ちました。ネタバレすると、結局出来ませんでした。(◞‸◟)しかし、色々Scalaの機能を使えたので、そういう勉強メモとして記事に残しておきます。(´・_・`)
制約版
いきなり実装するのは難しかったので、まず「case classのフィールド名は全部value」という制限がついたバージョンを作成しました。
まず、動作イメージからです。
package OptionValueAccessor import OptionValueAccessor._ object OptionValueAccessorTest { case class A(value: Int) case class B(value: Option[A]) case class C(value: Option[B]) case class D(value: Option[C]) case class E(value: D) def main(args: Array[String]): Unit = { val edcba = Some(E(D(Some(C(Some(B(Some(A(1))))))))) println(edcba.?.?.?.?.?) // Some(1) val ednon = Some(E(D(None))) println(ednon.?.?.?.?.?) // None } }
?.
というメソッド呼び出しはScala的にきつそうだったので.?
としました。まあこれでも大枠は問題ないはずです。
(ちなみに?
というメソッドを定義したい場合は、 def ?: T = ...
とするとエラーになるので、 def ? : T = ...
のように半角スペースを空けましょう)
上記のように.?
を「チェーンすることで内側の値にアクセスすることができ」、「途中でNoneが挟まっていたら途中で評価をやめてNoneを返す」ようになっています。?
だらけで読みにくいですけど・・・。(´・_・`)
このような動作を行うために、今回はマクロを使って?
メソッドを定義しました。
package OptionValueAccessor import scala.language.experimental.macros import scala.reflect.macros.whitebox.Context object OptionValueAccessor { implicit class OptValue[A, B](val self: Option[A])(implicit ev: A <:< {def value: B}) { def ? : Option[B] = macro Impl.optValImpl[A, B] } implicit class OptOpt[A, B](val self: Option[Option[A]])(implicit ev: A <:< {def value: B}) { def ? : Option[B] = macro Impl.optOptImpl[A, B] } object Impl { def optValImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: Context): c.Expr[Option[B]] = { import c.universe._ c.Expr[Option[B]](q"${c.prefix}.self.map {_.value}") } def optOptImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: Context): c.Expr[Option[B]] = { import c.universe._ c.Expr[Option[B]](q"${c.prefix}.self.flatten.map {_.value}") } }
https://gist.github.com/matsu-chara/702b7046eb890c4c0fe4
といっても、なんのことはなく?
メソッドを呼び出したインスタンスの型がOption[Option[A]
なのかOption[A]
なのかを見分けて、?
の部分をmap {_.value}
やflatten.map {_.value}
に置き換えているだけです。(ぶっちゃけマクロ要らないんですが、後ほどvalueの部分を可変にしたかったのでマクロが必要かな?と思ったので練習としてこちらもマクロで作りました。)
ちなみに、A <:< {def value: B}
のような型指定ですが、これはgeneralized type constraintsというもので、「Aがvalue: B
というメソッドを持っている」ときにだけ呼び出せるメソッドを定義できるものです。*2詳しくはScalaで<:<とか=:=を使ったgeneralized type constraintsがスゴすぎて感動した話 - ( ꒪⌓꒪) ゆるよろ日記で解説されています。
Type Dynamic
さきほどの例は.value
で固定されている上に?
だらけで読みにくかったので、なんとか?foo.?bar.?baz
のような呼び出しにしたいところです。しかし、Scalaでメソッド呼び出しを文字列で受け取って、別のメソッド呼び出しにすり替えた上に型チェックも出来るなんて無理だよなーしかたないよなーと思っていた所、
Type Dynamic を type safe に扱う方法 - seratch's weblog in Japaneseという記事でType Dynamicというまさにちょうど求めていたものがあることを発見しました。
これを使うと、以下のように_なんとか
というメソッドを呼び出すと、実際にはアンダースコアを切り落としたなんとか
というメソッドが呼ばれるような状況(しかもなんとか
が無い場合はコンパイルエラーになる状況)を作り出すことが出来ます。
やり方は簡単で、extends Dynamic
して、def selectDynamic(name: String)
を用意するだけ!
下記の例ではExmaple
クラスのvalue
フィールドにe._value
のようにアンダースコア付きでアクセスしています。(またしてもType Dynamicだけならマクロは不要ですが、フィールドが存在しなかった場合にコンパイルエラーにしたい場合は必要です。)*3
package TypeDynamic import scala.language.experimental.macros import scala.reflect.macros.whitebox import scala.reflect.macros.whitebox.Context object TypeDynamic { class Example() extends Dynamic { val value: Int = 2 def selectDynamic(name: String): Int = macro selectDynamicImpl } def selectDynamicImpl(c: whitebox.Context)(name: c.Expr[String]): c.Expr[Int] = { import c.universe._ // "_"を切り落とす val nameStr: String = name.tree match { case pq"${n: String}" if n.startsWith("_") => n.drop(1) case _ => c.abort(c.enclosingPosition, s"#$name does not start with _") } c.Expr[Int](q"${c.prefix}.${TermName(nameStr)}") } }
実行用のコードはこちら。
package TypeDynamic import TypeDynamic._ object TypeDynamicTest { def main(args: Array[String]): Unit = { val e = new Example() println(e._value) // e.valueが呼び出されて2が表示される。 } }
https://gist.github.com/matsu-chara/e34bc9b03674a20c9f41
?メソッドとType Dynamicの融合
Type Dynamicと?
メソッドを組み合わせればいい感じに行けるじゃーん₍₍ (ง ˘ω˘ )ว ⁾⁾と思ったのですが、Scalaでは?hoge
のような?の後に何かが続くメソッド呼び出しが認められていませんでした。(´・_・`)
しかたがないので_hoge
で呼び出すとオプションを突き抜けてアクセスできるようにすることにしました。
実装例はこちらです。
package OptionValueAccessor import scala.language.experimental.macros import scala.reflect.macros.whitebox object OptionValueAccessor { implicit class OptValue[A, B](val self: Option[A])(implicit ev: A <:< {def value: B}) extends Dynamic { def selectDynamic(name: String): Option[B] = macro Impl.Opt.selectDynamicImpl[B] } implicit class OptOpt[A, B](val self: Option[Option[A]])(implicit ev: A <:< {def value: B}) { def selectDynamic(name: String): Option[B] = macro Impl.OptOpt.selectDynamicImpl[B] } object Impl { object Opt { def selectDynamicImpl[B: c.WeakTypeTag](c: whitebox.Context)(name: c.Expr[String]): c.Expr[Option[B]] = { import c.universe._ val nameStr: String = name.tree match { case pq"${n: String}" if n.startsWith("_") => n.drop(1) case _ => c.abort(c.enclosingPosition, s"#$name not found.") } c.Expr[Option[B]](q"${c.prefix}.self.map {_.${TermName(nameStr)}}") } } object OptOpt { def selectDynamicImpl[B: c.WeakTypeTag](c: whitebox.Context)(name: c.Expr[String]): c.Expr[Option[B]] = { import c.universe._ val nameStr: String = name.tree match { case pq"${n: String}" if n.startsWith("_") => n.drop(1) case _ => c.abort(c.enclosingPosition, s"#$name not found.") } c.Expr[Option[B]](q"${c.prefix}.self.flatten.map {_.${TermName(nameStr)}}") } } } }
しかし、これ思ったようには動きませんでした。(◞‸◟)
一応、動作するコード例はこちらです。
package OptionValueAccessor import OptionValueAccessor._ object OptionValueAccessorTest { case class A(value: Int) case class B(value: Option[A]) case class C(value: Option[B]) case class D(value: Option[C]) case class E(value: D) def main(args: Array[String]): Unit = { val edcba: Option[E] = Some(E(D(Some(C(Some(B(Some(A(1))))))))) println( edcba .selectDynamic("_value") .selectDynamic("_value") .selectDynamic("_value") .selectDynamic("_value") .selectDynamic("_value") ) val edcbnone = Some(E(D(None))) println( edcbnone .selectDynamic("_value") .selectDynamic("_value") .selectDynamic("_value") .selectDynamic("_value") .selectDynamic("_value") ) } }
https://gist.github.com/matsu-chara/de8b85b6bf21f5fa977c
なんやこのselectDynamic地獄は!意味ないやんけ!
ちゃんと調べきれてないので確証が全く無いのですが、「Type Dynamicによるメソッド呼び出しのselectDynamic
への変換」と「implicit classによる暗黙の型変換」は両立しないみたい・・・?です。(調べた限りで、「出来ない」という記述はなかったのでもしかしたらできるかもしれません。)
もう一つ問題があって、OptValue
とOptOptValue
の定義に{def value: B}として、フィールド名をハードコーディングしているので、前述の問題が解消してもまだ予定通りには動きません。これもどうやれば回避できるのかいまいちアイデアがありませんでした。
課題
以上のような感じで非常に中途半端なのですが、現状の課題点をまとめます。
CoffeeScriptの存在演算子は
?.
だが、今回作れたのは.?
。?hoge
のようなメソッド呼び出しが出来ないので_hoge
で妥協。Type Dynamicを使おうとしてもimplicit conversionと両立しない(?)ため、明示的にselectDynamicを呼び出す必要がある。
def hoge: X
のような構造的部分型を使用しているので、Type Dynamicを使っても結局汎用的なフィールドには使えない。(大量のimplicit classを定義すればフィールド名が1~5文字なら使えるみたいなことはできるかも。)
うーん、これはちょっと課題山積みでめんどくさくなってきたクリアするのが難しそうなので今回はこの辺で終わりとします。(´・_・`)
もしかしたらインターフェースの変更(たとえば .?hoge
を諦めて?("hoge")
とする。)や、もうちょっと何かテクニックを使ったり、初歩的な見落としを発見すればいけるかもしれません・・・。
参考文献
assertEqualのサンプルを、そもそものScalaマクロの書き方の参考にしました。 Scalaのマクロの基礎。評価タイミング、評価回数と、健全性 - scalaとか・・・
公式のマクロドキュメント Macros - ユースケース - Scala Documentation
型制約に使ったgeneralized type constraintsについて(友達に教えてもらいました。) Scalaで<:<とか=:=を使ったgeneralized type constraintsがスゴすぎて感動した話 - ( ꒪⌓꒪) ゆるよろ日記
Type Dynamicについて Type Dynamic を type safe に扱う方法 - seratch's weblog in Japanese
*1:元の問題と違うじゃん!というつっこみについては後述します・・・。
*2:Option#flattenなんかもこの制約を使っていて、flatten自体はOptionの他のメソッド(例えばisEmptyなど)と一緒に定義されているのですが、flattenは、この制約を使うことでAがOption[B]のときのみ(つまりOption[A] = Option[Option[B]のようにネストしているときのみ)呼び出せるメソッド定義になっています。
*3:今回はチェックが適当なのでコード上で、フィールドを確認してabort!みたいなことはしてないのですが、一応エラーメッセージがわかりにくいだけでエラーにはなります。