Last active
March 13, 2017 06:09
-
-
Save matsu-chara/f58868ebc99bffabe4c385b3c7b01170 to your computer and use it in GitHub Desktop.
get field name of case class and get type checked new value. (scalameta 1.6.0, paradise 3.0.0-M7)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
) | |
} |
普通のmacroをかく観点からすると
- scala.metaに
TermName(c.freshName())
みたいなのないんですか?(ローカル変数や引数名でも念のため全部やったほうがよい?) String
もちゃんと_root_.scala.String
みたいに、全部full nameで参照するべき?
などがあるが、scala.metaだと実は違う(やらなくていい?)のか、よく知らない
ありがとうございます!
value
直書き部分をTerm.fresh("value")
に変更- Stringをfull name参照に変更
しました!
型の参照はtypeOf[String]
みたいにするのがスマートな気がしますね。scala.metaで使えるのか知りませんが
なるほど・・!
http://scalameta.org/tutorial/ が更新されてるのか不明ですが _root.~
で参照しているっぽいのでダメそうですかね・・(◞‸◟)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
モチベーションが伝わりづらいけど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になやまされたのでテクニックを知っていれば多分取れそう。)