読者です 読者をやめる 読者になる 読者になる

Scalaに存在演算子を求めるのは間違っているだろうか

追記

間違っていないことを示していただけました →

「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で&lt;:&lt;とか=:=を使った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による暗黙の型変換」は両立しないみたい・・・?です。(調べた限りで、「出来ない」という記述はなかったのでもしかしたらできるかもしれません。)

もう一つ問題があって、OptValueOptOptValueの定義に{def value: B}として、フィールド名をハードコーディングしているので、前述の問題が解消してもまだ予定通りには動きません。これもどうやれば回避できるのかいまいちアイデアがありませんでした。

課題

以上のような感じで非常に中途半端なのですが、現状の課題点をまとめます。

  • CoffeeScriptの存在演算子?.だが、今回作れたのは.?

  • ?hogeのようなメソッド呼び出しが出来ないので_hogeで妥協。

  • Type Dynamicを使おうとしてもimplicit conversionと両立しない(?)ため、明示的にselectDynamicを呼び出す必要がある。

  • def hoge: X のような構造的部分型を使用しているので、Type Dynamicを使っても結局汎用的なフィールドには使えない。(大量のimplicit classを定義すればフィールド名が1~5文字なら使えるみたいなことはできるかも。)

うーん、これはちょっと課題山積みでめんどくさくなってきたクリアするのが難しそうなので今回はこの辺で終わりとします。(´・_・`)

もしかしたらインターフェースの変更(たとえば .?hogeを諦めて?("hoge")とする。)や、もうちょっと何かテクニックを使ったり、初歩的な見落としを発見すればいけるかもしれません・・・。

参考文献

*1:元の問題と違うじゃん!というつっこみについては後述します・・・。

*2:Option#flattenなんかもこの制約を使っていて、flatten自体はOptionの他のメソッド(例えばisEmptyなど)と一緒に定義されているのですが、flattenは、この制約を使うことでAがOption[B]のときのみ(つまりOption[A] = Option[Option[B]のようにネストしているときのみ)呼び出せるメソッド定義になっています。

*3:今回はチェックが適当なのでコード上で、フィールドを確認してabort!みたいなことはしてないのですが、一応エラーメッセージがわかりにくいだけでエラーにはなります。