使用代码生成(如Scala Meta)来废弃样板

时间:2017-09-01 17:28:30

标签: scala macros code-generation scala-meta

我使用Shapeless标记的类型来获取好的类型安全原语来传递我的业务逻辑。定义这些类型的方法很简单:

sealed trait MyTaggedStringTag
type MyTaggedString = String @@ MyTaggedStringTag

但是我已经为此添加了一些辅助逻辑,现在我的定义看起来更像:

sealed trait MyTaggedStringTag

type MyTaggedString = String @@ MyTaggedStringTag
object MyTaggedString {
  def fromString(untaggedMyTaggedString: String): MyTaggedString = {
    val myTaggedString = tag[MyTaggedStringTag](untaggedMyTaggedString)
    myTaggedString
  }
}
implicit class MyTaggedStringOps(val myTaggedString: MyTaggedString) extends AnyVal { def untagged = myTaggedString.asInstanceOf[String] }

因此,根据定义,它有很多样板。我真的希望通过做类似的事情来产生这个:

@tagged[String] type MyTaggedString

有没有办法用Scala Meta或其他代码生成工具做这样的事情?

1 个答案:

答案 0 :(得分:1)

<强>更新

现在已经完全正常工作,可以在我称之为Taggy的新库中看到。这是宏的最新版本:

class tagged extends scala.annotation.StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    // Macro annotation type and value parameters come back as AST data, not 
    // values, and are accessed by destructuring `this`.
    defn match {
      case q"..$mods type $newType = ${underlyingType: Type.Name}" => 
        TaggedImpl.expand(underlyingType, newType, mods)
      case _ => 
        abort("Correct usage: @tagged type NewType = UnderlyingType" )
    }
  }
}

object TaggedImpl {
  def expand(underlyingType: Type.Name, newType: Type.Name, mods: Seq[Mod]) = {
    // Shapeless needs a phantom type to join with the underlying type to
    // create our tagged type. Ideally should never leak to external code.
    val tag = Type.Name(newType.value + "Tag")

    // The `fromX` helper will go in the companion object.
    val companionObject = Term.Name(newType.value)

    // We'll name the `fromX` method based on the underlying type.
    val fromMethod = Term.Name("from" + underlyingType.value)

    // The `untagged` helper goes in an implicit class, since the tagged type
    // is only a type alias, and can't have real methods. 
    val opsClass = Type.Name(newType.value + "Ops")

    q"""
      sealed trait $tag
      ..$mods type $newType = com.acjay.taggy.tag.@@[$underlyingType, $tag]
      ..$mods object $companionObject {
        def $fromMethod(untagged: $underlyingType): $newType = {
          val tagged = com.acjay.taggy.tag[$tag](untagged)
          tagged
        }
      }
      ..$mods implicit class $opsClass(val tagged: $newType) extends AnyVal { 
        def untagged = tagged.asInstanceOf[$underlyingType]
        def modify(f: $underlyingType => $underlyingType) = $companionObject.$fromMethod(f(untagged))
      }
    """
  }
}

object tag {
  def apply[U] = new Tagger[U]

  trait Tagged[U]
  type @@[+T, U] = T with Tagged[U]

  class Tagger[U] {
    def apply[T](t : T) : T @@ U = t.asInstanceOf[T @@ U]
  }
}

为了便于阅读,分离宏语法和代码生成的解析。您可以将TaggedImpl.expand内联到meta块中。另请注意,此处的语法现在为@tagged type MyTaggedString = String

原始回答

我把它作为概念证明。但它需要基础类型的字符串名称:

import scala.meta._

class tagged(_underlyingTypeName: String) extends scala.annotation.StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    // Can't figure out how to do this extraction as a quasiquote, so I 
    // figured out exactly the AST `this` produces to extract the string 
    // parameter.
    val Term.New(
      Template(
        List(),
        List(Term.Apply(Ctor.Ref.Name("tagged"), List(Lit.String(underlyingTypeName)))),
        Term.Param(List(), Name.Anonymous(), None, None),
        None
      )
    ) = this

    val q"..$mods type $tname[..$tparams]" = defn
    val underlyingType = Type.Name(underlyingTypeName)
    TaggedImpl.expand(tname, underlyingType)
  }
}

object TaggedImpl {
  def expand(taggedType: Type.Name, underlyingType: Type.Name) = {
    val tag = Type.Name(taggedType.value + "Tag")
    val companionObject = Term.Name(taggedType.value)
    val fromMethodName = Term.Name("from" + underlyingType.value)
    val opsClass = Type.Name(taggedType.value + "Ops")

    q"""
      sealed trait $tag
      type $taggedType = shapeless.tag.@@[$underlyingType, $tag]
      object $companionObject {
        def $fromMethodName(untagged: $underlyingType): $taggedType = {
          val tagged = shapeless.tag[$tag](untagged)
          tagged
        }
      }
      implicit class $opsClass(val tagged: $taggedType) extends AnyVal { 
        def untagged = tagged.asInstanceOf[$underlyingType] 
      }
    """
  }
}