独学大学情報学部

主にScala、たまにHaskell、稀に数学

Platedとは何なのか?

この記事はadventar版Scalaアドベントカレンダーの21日目です。
担当は@aoiroaoinoです。17日ズサーしてる場合じゃなかった。とっても遅刻しちゃってすみません。。

まえがき

いつも通りMonocleの話です。事の発端はこのissueとPRでした。

https://github.com/julien-truffaut/Monocle/issues/288 https://github.com/julien-truffaut/Monocle/pull/289

全く知らなかったのだけど、追加されたexampleやらtestやら眺めてたら便利そうだったので。

Platedのインスタンス

まず、Plated自体は型クラスで定義は以下のとおり。

abstract class Plated[A] extends Serializable { self =>
  def plate: Traversal[A, A]
}

Plated自体はTraversal[A, A]なplateを一つ要求してるだけです。本当にこれだけ。 で、Traversalは以下のように定義されています。

type Traversal[S, A] = PTraversal[S, S, A, A]
abstract class PTraversal[S, T, A, B] extends Serializable { self =>

  /**
   * modify polymorphically the target of a [[PTraversal]] with an Applicative function
   * all traversal methods are written in terms of modifyF
   */
  def modifyF[F[_]: Applicative](f: A => F[B])(s: S): F[T]

  ...
}

つまり、Platedのインスタンスが要求してるのはdef modifyF[F[_]: Applicative](f: A => F[B])(s: S): F[T]という メソッドを定義しろって言ってるだけです。

で、Monocle(v1.2.0 現在)で定義されてるインスタンスは以下の8つです。

要するにTraversalを定義できればいいので、例えば自前で定義した型のインスタンスを定義することもできます。 例えばexampleにあるJsonの例を参考に、json4sのJsonASTに対して定義してみるとこんな感じ。*1

implicit def jValuePlated: Plated[JValue] = new Plated[JValue] {
  val plate: Traversal[JValue, JValue] = new Traversal[JValue, JValue] {
    def modifyF[F[_]: Applicative](f: JValue => F[JValue])(x: JValue): F[JValue] =
      x match {
        case j@(JString(_) | JBool(_) | JDouble(_) | JDecimal(_) | JInt(_)| JNull | JNothing) => Applicative[F].point(j)
        case JArray(a) => a.traverse(f).map(JArray)
        case JObject(o) => o.toMap.traverse(f).map(_.toList).map(JObject.apply)
      }
  }
}

で、インスタンスの定義方法をだらだら書きましたが、んじゃこんなの定義して何が嬉しいのかって話です。

Platedの使い方

Plated自体はインスタンスになってるデータ型に対して以下の関数を提供します。 ※見やすさの為にコメントやら実装やらを消しました。元のコードはこちらを参照してください。

trait PlatedFunctions {

  def children[A: Plated](a: A): List[A]

  def universe[A: Plated](a: A): Stream[A]

  def rewrite[A: Plated](f: A => Option[A])(a: A): A

  def rewriteOf[A](l: Setter[A, A])(f: A => Option[A])(a: A): A

  def transform[A: Plated](f: A => A)(a: A): A

  def transformOf[A](l: Setter[A, A])(f: A => A)(a: A): A
}

で、今回はとりあえずrewritetransformの使い方(?)だけざっくり。 対象のデータ型はJsonが分かりやすそうなので、上で定義したjson4sのやつを使います。 ※ちゃんと動くサンプルコードはこちら

操作する対象のjsonはこんな感じ。

val json: JValue = JObject(
  "name"      -> JString("aoino"),
  "age"       -> JInt(25),
  "favorites" -> JArray(List(
    JString("scala"),
    JString("haskell")
  )),
  "address"   -> JObject(List(
    "country"  -> JString("japan"),
    "city"     -> JString("tokyo"),
    "postcode" -> JInt(12345)
  ))
)

rewrite

例えばJValueの場合はdef rewrite[JValue](f: JValue => Option[JValue])(a: JValue): JValueとなりますね。 第二引数で受け取ったa: JValueの要素に対してfを適用していき、Someだったら再度適用、Noneだったら次の要素というのを繰り返し、それを全要素に対して行います。畳み込みの様な振る舞いをします。

it("rewrite: Apply toUpperCase to all JString") {
  Plated.rewrite[JValue] {
    case JString(s) =>
      val x = s.toUpperCase
      if (s != x) Some(JString(x)) else None
    case _ => None
  }(json) shouldEqual JObject(
    "name"      -> JString("AOINO"),
    "age"       -> JInt(25),
    "favorites" -> JArray(List(
      JString("SCALA"),
      JString("HASKELL")
    )),
    "address"   -> JObject(List(
      "country"  -> JString("JAPAN"),
      "city"     -> JString("TOKYO"),
      "postcode" -> JInt(12345)
    ))
  )
}

transform

def transform[JValue](f: JValue => JValue)(a: JValue): JValue こいつも同様に全要素を走査するんですが、rewriteと違って、fを適用した結果に関わらず次の要素に進みます。 おなじみのmapの様な振る舞いをします。

it("transform: JInt to JDouble") {
  Plated.transform[JValue] {
    case JInt(i) => JDouble(i.toDouble)
    case x       => x
  }(json) shouldEqual JObject(
    "name"      -> JString("aoino"),
    "age"       -> JDouble(25.0),
    "favorites" -> JArray(List(
      JString("scala"),
      JString("haskell")
    )),
    "address"   -> JObject(List(
      "country"  -> JString("japan"),
      "city"     -> JString("tokyo"),
      "postcode" -> JDouble(12345.0)
    ))
  )
}

で、これを知って一層某MonadicJValueとかいうclassに対して何がもなでぃっくじゃーみたいな気分になれるわけです。

結局Platedは何なのか?どこから来たのか?

本当はここをメインに書きたかったんですけど、掲載日過ぎた今日現在でもあんまりちゃんと理解できてないので調べた内容だけ。

issueにも貼られていたけど、Monocleの実装の元になったControl.Lens.Platedで、概要を読んでみると、どうやら Scrap Your Boilerplate の話で、Neil Mitchell さんの Uniplateが元になってるっぽい。元の論文は同氏の Uniform Boilerplate and List Processing で、どうやら2007年開催のHaskell Workshop (?)で発表されたものらしい。はい、ここまでです。

まとめ

見つけた肝心の論文に関しては読み始めたものの、相も変わらずぐうの音も出ない状態なので、皆さんなんでそんなにカジュアルにラノベ感覚で論文読めるんですかっ???って気分になったので、引き続き頑張りますとだけ...。まさかLens楽しくて追っかけてたらSYBに至るとは思ってなかったので、しばらくはSYB関連調べて浸ってみるのも良さそうってのが感想です。元々気にはなってたしね。あと来年はHaskellと英語頑張るのが良さそうですね。良いお年を。(早