magnoliaで素振り

magnoliaは型クラスのインスタンス自動導出のためのscalaライブラリです。 GitHub - propensive/magnolia: A better generic macro for Scala

既にscalaz-magnoliascalacheck-magnolia などがリリースされています。

公式チュートリアルもしっかりとあり、それに加えて特に何かというわけではないんですが最近本ばっかり読んでてあんまり色々いじれてないので個人の素振り備忘録です。 (つまりこの記事よりも Magnolia: Home を見たほうが良いです)

magnolia特長

型クラスの自動導出自体はshapelessでもできますが、Genに移した後HListとCoproduct用のインスタンスを定義して〜とやると意外とコード量が増えたりコンパイルエラーが起きてデバッグする時間が多くなったりします。

magnoliaを使うと以下のようなメリットが得られます。

  • 短く書ける
  • 導出に失敗した際のデバッグメッセージが詳しく出る
  • shapelessに比べて4~15倍速いcompile時間

ただフリーランチというわけではなく以下のような弱点もあります。

  • まだexperimentalなので色々変わるかもしれない
  • 一部型チェック諦めてるのでClassCastExceptionが実行時に出る可能性がある
  • 現時点ではhigher-kindな型クラス(Functor)の導出はできない

やってみた

チュートリアルとほぼ内容同じなので解説はそっち読んで貰えればと思います。 チュートリアルの延長線上でStringじゃなくて型名とパラメータ名を構造化してほしいなって思ったのでMapDumpみたいな物を作ってみました。 https://github.com/matsu-chara/magnolia-example

package example.domain

case class UserId(value: Long) extends AnyVal
case class UserName(value: String)

sealed trait UserType
object UserType {
  case object Normal extends UserType
  case object Premium extends UserType
}

case class User(
  id: UserId,
  name: UserName,
  tpe: UserType
)
package example

import example.domain.{User, UserId, UserName, UserType}
import example.dump.MapDump

object Main extends App {
  val u1 = User(UserId(1L), UserName("sato"), UserType.Normal)

//  if derivation failed, then output
//  Main.scala:11:22: magnolia: could not find MapDump.Typeclass for type Long
//       in parameter 'value' of product type example.domain.UserId
//       in parameter 'id' of product type example.domain.User
  println(MapDump.gen[User].dumpAsMap(u1))

//  same as above. but no debug output
//  (User,Map(id -> (UserId,UserId(1)), name -> (UserName,Map(value -> (string,sato))), tpe -> (Normal,Normal)))
  println(implicitly[MapDump[User]].dumpAsMap(u1))
}
package example.dump

import magnolia._

import scala.language.experimental.macros

trait MapDump[A] {
  /** return
    * (className, Map(param1 -> value1, param2 -> value2))
    * or
    * (className, value1)
    * when value class or object
    */
  def dumpAsMap(value: A): (String, Any)
}

object MapDump extends GenericMapDump {
  def apply[A](f: A => (String, Any)): MapDump[A] = new MapDump[A] {
    override def dumpAsMap(value: A): (String, Any) = f(value)
  }

  implicit val stringDump: MapDump[String] = MapDump[String] { value =>
    ("string", value)
  }

  implicit val longDump: MapDump[Long] = MapDump[Long] { value =>
    ("long", value)
  }
}

trait GenericMapDump {
  type Typeclass[T] = MapDump[T]

  def combine[T](ctx: CaseClass[Typeclass, T]): Typeclass[T] = new Typeclass[T] {
    override def dumpAsMap(value: T): (String, Any) = {
      val valueOrMap = if (ctx.isValueClass || ctx.isObject) {
        value
      } else {
        ctx.parameters.map { p =>
          p.label -> p.typeclass.dumpAsMap(p.dereference(value))
        }.toMap
      }
      (ctx.typeName.short, valueOrMap)
    }
  }

  def dispatch[T](ctx: SealedTrait[Typeclass, T]): Typeclass[T] = new Typeclass[T] {
    override def dumpAsMap(value: T): (String, Any) = ctx.dispatch(value) { sub =>
      sub.typeclass.dumpAsMap(sub.cast(value))
    }
  }

  implicit def gen[T]: Typeclass[T] = macro Magnolia.gen[T]
}

この型クラス何の役に立つんだろう・・・?というツッコミとか色々有る気がするけどとりあえず動いてるのでセーフ。

せっかくなのでdecodeも作ってみました

package example

import example.domain.{User, UserId, UserName, UserType}
import example.dump.{MapDump, FromDumpedMap}

object Main extends App {
  val u1 = User(UserId(1L), UserName("sato"), UserType.Normal)

  // User(UserId(1),UserName(sato),Normal)
  val dumped = MapDump.gen[User].dumpAsMap(u1)
  println(FromDumpedMap.gen[User].constructFrom(dumped))
}
package example.dump

import magnolia._

import scala.language.experimental.macros
import scala.util.control.NonFatal

trait FromDumpedMap[A] {
  def constructFrom(value: (String, Any)): A
}

object FromDumpedMap extends GenericFromDumpedMap {
  def apply[A](f: (String, Any) => A): FromDumpedMap[A] =
    new FromDumpedMap[A] {
      override def constructFrom(value: (String, Any)): A = f(value._1, value._2)
    }

  implicit val stringDump: FromDumpedMap[String] = FromDumpedMap[String] {
    case (clazz, param: String) if clazz == "string" =>
      param
    case arg =>
      throw new IllegalArgumentException(s"failed to decode. $arg")
  }

  implicit val longDump: FromDumpedMap[Long] = FromDumpedMap[Long] {
    case (clazz, param: Long) if clazz == "long" =>
      param
    case arg =>
      throw new IllegalArgumentException(s"failed to decode. $arg")
  }
}

trait GenericFromDumpedMap {
  type Typeclass[T] = FromDumpedMap[T]

  def combine[T](ctx: CaseClass[Typeclass, T]): Typeclass[T] = new Typeclass[T] {
    override def constructFrom(value: (String, Any)): T = {
      if (ctx.isValueClass || ctx.isObject) {
        try {
          value._2.asInstanceOf[T]
        } catch {
          case NonFatal(e) => throw new IllegalArgumentException(s"failed to decode. $ctx $value", e)
        }
      } else {
        ctx.construct { p =>
          val paramMap = try {
            value._2.asInstanceOf[Map[String, (String, Any)]]
          } catch {
            case NonFatal(e) => throw new IllegalArgumentException(s"failed to decode. $ctx $value", e)
          }

          val param = if (paramMap.contains(p.label)) {
            paramMap(p.label)
          } else {
            throw new IllegalArgumentException(s"failed to decode. $ctx $p $value")
          }
          p.typeclass.constructFrom(param)
        }
      }
    }
  }

  def dispatch[T](ctx: SealedTrait[Typeclass, T]): Typeclass[T] = new Typeclass[T] {
    override def constructFrom(value: (String, Any)): T = {
      val (subtype, subTypeValue) = ctx.subtypes.find(_.typeName.short == value._1) match {
        case Some(sub) =>
          try {
            (sub, value._2.asInstanceOf[sub.SType])
          } catch {
            case NonFatal(e) => throw new IllegalArgumentException(s"failed to decode. $ctx $value", e)
          }
        case _ => throw new IllegalArgumentException(s"failed to decode. $ctx $value")
      }
      subtype.typeclass.constructFrom((value._1, subTypeValue))
    }
  }

  implicit def gen[T]: Typeclass[T] = macro Magnolia.gen[T]
}

この形式をデシリアライズしたい人間は居るのか?というところは置いといて出来てそうです。 (そしてちゃんとやるならStringじゃなくてTypeNameとかParamNameにしたほうが良い)

consturctメソッドや中で呼んでるrawConstructメソッド的にdecode失敗したらEitherで返すみたいなことはできないっぽいので、そういうことがやりたい場合はまだ難しそうですね。

ある程度パターン化できてるので、さっくりderivingしたいだけの時はかなり便利そうです。(decodeとか書くとdecode自体が面倒なのでさくっとという範疇ではない気がしますがそれはdecode自体の問題)