Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
get field name of case class and get type checked new value. (scalameta 1.6.0, paradise 3.0.0-M7)
package scalaworld.macros
package com.folio.account.macros
import scala.collection.immutable.Seq
import scala.meta._
/**
* annotation for case class.
*
* It generates `FieldNameAndValuePorter` obect in comanion obect, that have these methods for each case class field.
* `def fieldName(value: TypeOfField): (String, TypeOfField) = (fieldName, value)`
*
* @example
* {{{
* @FieldNameAndValuePorter
* case class Mofu(foo: Int, bar: String)
* obect Mofu {}
*
* Mofu.FieldNameAndValuePorter.foo(1) ==> ("foo", 1)
* Mofu.FieldNameAndValuePorter.bar(2) ==> type error Int is not String
* }}}
*/
class FieldNameAndValuePorter extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
def createPorterObject(paramss: Seq[Seq[Term.Param]]): Defn.Object = {
val porters = paramss.flatten.map { param =>
val fieldName = Term.Name(param.name.syntax)
val fieldType = param.decltpe.getOrElse(abort(s"${param.name.syntax} does not have type"))
val argName = Term.fresh("value")
val returnType = Type.Name(fieldType.syntax)
q"""def $fieldName($argName: $fieldType):(_root_.scala.Predef.String, $returnType) = (${fieldName.value}, $argName)"""
}
q"""object FieldNameAndValuePorter { ..$porters }"""
}
defn match {
// companion object exists
case Term.Block(Seq(cls @ Defn.Class(_, name, _, ctor, _), companion: Defn.Object)) =>
val porterObject = createPorterObject(cls.ctor.paramss)
val newCompanion = companion.copy(
templ = companion.templ.copy(
stats = Some(porterObject +: companion.templ.stats.getOrElse(Nil))
)
)
Term.Block(Seq(cls, newCompanion))
// companion object does not exists
case cls: Defn.Class =>
val porterObject = createPorterObject(cls.ctor.paramss)
val companion = q"object ${Term.Name(cls.name.value)} { $porterObject }"
Term.Block(Seq(cls, companion))
// something wrong
case _ =>
println(defn.structure)
abort("@FieldNameAndValuePorter must annotate a object.")
}
}
}
package scalaworld.macros
import org.scalatest.FunSuite
object Mofu
@FieldNameAndValuePorter
case class Mofu(wan: Int, nyan: String, gau: Double, oh: String)
class FieldNameAndValuePorterMain extends FunSuite {
test("mofu") {
// ("wan",2)
println(Mofu.MacroPorter.wan(2))
// ("nyan", "nyaa")
println(Mofu.MacroPorter.nyan("nyaa"))
// compile error gyan not defined
// println(Mofu.MacroPorter.gyan(3))
// compile error wan is not string
// println(Mofu.MacroPorter.wan("waon"))
}
}
obect MofuSql {
def update(columnAndValues: Map[Any, String]): Future[Unit] = {
someLibrarySql.update(columnAndValues)
}
}
object MofuMain {
val (wanValue, nyanValue) = parseSomeRequest()
// voilerplate
MofuSql.update(Map(
"wan" -> wanValue,
"nyn" -> nyanValue, // typo!
"gau" -> "gaugau" // type unsafe (String is not Double)
))
// macro (typo safe, value is type checked)
MofuSql.update(Map(
Mofu.MacroPorter.wan(wanValue),
Mofu.MacroPorter.nyan(nyanValue),
Mofu.MacroPorter.gau(1.3)
)
}
Owner

matsu-chara commented Feb 27, 2017

モチベーションが伝わりづらいけどDBへのアップデートでフィールドを4つか5つ指定したい(かつcase classのインスタンスは情報が足りなくて作れないという制約がある)という制約を考える。

このとき sql.update(テーブル, Map[カラム名 -> 更新する値]) のようなインターフェースがあるとするとMap[フィールド名 => Any] のようなものが必要になる。
このマップは例えば User(id: Long, tpe: Int, name: String) では Map("id" -> 0L, tpe: 1, name: "モフたろう") のようなものになる。

フィールド名を手書きするのは嫌だし、idに間違えてStringを渡してしまうことも避けたいので (フィールド名, そのフィールドに応じた型) というタプルを型安全に作ってからMap[String, Any]を生成する方針にしたい。

ということで Mofu.wan(value = 1) のようにすると型チェックされた上で "wan" -> 1 がかえってくるマクロが誕生しました。
(shapelessのLensを使えばフィールド名の取得は行けそうだったけど、渡されたフィールドの型に応じた型をチェックするのが難しかった。LabelledGenericもLensもインスタンスがないとフィールドの型チェックが難しそうにみえた。情報としては揃っていてcan not proveになやまされたのでテクニックを知っていれば多分取れそう。)

xuwei-k commented Feb 28, 2017

普通のmacroをかく観点からすると

  • scala.metaに TermName(c.freshName()) みたいなのないんですか?(ローカル変数や引数名でも念のため全部やったほうがよい?)
  • String もちゃんと _root_.scala.String みたいに、全部full nameで参照するべき?

などがあるが、scala.metaだと実は違う(やらなくていい?)のか、よく知らない

Owner

matsu-chara commented Feb 28, 2017

ありがとうございます!

  • value 直書き部分を Term.fresh("value") に変更
  • Stringをfull name参照に変更

しました!

型の参照はtypeOf[String]みたいにするのがスマートな気がしますね。scala.metaで使えるのか知りませんが

Owner

matsu-chara commented Feb 28, 2017

なるほど・・!
http://scalameta.org/tutorial/ が更新されてるのか不明ですが _root.~ で参照しているっぽいのでダメそうですかね・・(◞‸◟)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment