magnoliaは型クラスのインスタンス自動導出のためのscalaライブラリです。 GitHub - propensive/magnolia: A better generic macro for Scala
既にscalaz-magnoliaやscalacheck-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自体の問題)