如何在Dotty宏中访问案例类的参数列表 [英] How to access parameter list of case class in a dotty macro

查看:94
本文介绍了如何在Dotty宏中访问案例类的参数列表的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试学习dotty的元编程.具体编译时间码生成.我认为通过构建一些东西来学习将是一个很好的方法.因此,我决定制作一个CSV解析器,该解析器会将行解析为case类.我想使用点子宏来生成解码器

trait Decoder[T]{
  def decode(str:String):Either[ParseError, T]
}

object Decoder {
  inline given stringDec as Decoder[String] = new Decoder[String] {
    override def decode(str: String): Either[ParseError, String] = Right(str)
  }

  inline given intDec as Decoder[Int] = new Decoder[Int] {
    override def decode(str: String): Either[ParseError, Int] =
      str.toIntOption.toRight(ParseError(str, "value is not valid Int"))
  }
  
  inline def forType[T]:Decoder[T] = ${make[T]}

  def make[T:Type](using qctx: QuoteContext):Expr[Decoder[T]] = ???
}

我已经为Int& String,现在我正在寻找def make[T:Type]方法的指南.如何在此方法内迭代案例类T的参数列表?有建议的方式或模式吗?

解决方案

使用标准类型类推导在Dotty中

import scala.deriving._
import scala.compiletime._

case class ParseError(str: String, msg: String)
  
trait Decoder[T]{
  def decode(str:String): Either[ParseError, T]
}

object Decoder {
  inline given stringDec as Decoder[String] = new Decoder[String] {
    override def decode(str: String): Either[ParseError, String] = Right(str)
  }

  inline given intDec as Decoder[Int] = new Decoder[Int] {
    override def decode(str: String): Either[ParseError, Int] =
      str.toIntOption.toRight(ParseError(str, "value is not valid Int"))
  }

  inline derived[T](using m: Mirror.Of[T]): Decoder[T] = {
    val elemInstances = summonAll[m.MirroredElemTypes]
    inline m match {
      case p: Mirror.ProductOf[T] => productDecoder(p, elemInstances)
      case s: Mirror.SumOf[T]     => ???
    }
  }

  inline def summonAll[T <: Tuple]: List[Decoder[_]] = inline erasedValue[T] match {
    case _: Unit /* EmptyTuple in 0.25 */ => Nil
    case _: (t *: ts) => summonInline[Decoder[t]] :: summonAll[ts]
  }

  def productDecoder[T](p: Mirror.ProductOf[T], elems: List[Decoder[_]]): Decoder[T] =
    new Decoder[T] {
      def decode(str: String): Either[ParseError, T] = {
        elems.zip(str.split(','))
          .map(_.decode(_).map(_.asInstanceOf[AnyRef]))
          .sequence
          .map(ts => p.fromProduct(new ArrayProduct(ts.toArray)))
      }
    }

  def [E,A](es: List[Either[E,A]]) sequence: Either[E,List[A]] =
    traverse(es)(x => x)

  def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
    es.foldRight[Either[E, List[B]]](Right(Nil))((h, tRes) => map2(f(h), tRes)(_ :: _))

  def map2[E, A, B, C](a: Either[E, A], b: Either[E, B])(f: (A, B) => C): Either[E, C] = 
    for { a1 <- a; b1 <- b } yield f(a1,b1)
}

case class A(i: Int, s: String) derives Decoder
  
@main def test = {
  println(summon[Decoder[A]].decode("10,abc"))//Right(A(10,abc))
  println(summon[Decoder[A]].decode("xxx,abc"))//Left(ParseError(xxx,value is not valid Int))
  // println(summon[Decoder[A]].decode(","))
}

在0.24.0中进行了测试.


使用 Shapeless-3

import shapeless.{K0, Typeable}

case class ParseError(str: String, msg: String)

trait Decoder[T]{
  def decode(str:String): Either[ParseError, T]
}

object Decoder {
  inline given stringDec as Decoder[String] = new Decoder[String] {
    override def decode(str: String): Either[ParseError, String] = Right(str)
  }

  inline given intDec as Decoder[Int] = new Decoder[Int] {
    override def decode(str: String): Either[ParseError, Int] =
      str.toIntOption.toRight(ParseError(str, "value is not valid Int"))
  }

  inline def derived[A](using gen: K0.Generic[A]): Decoder[A] =
    gen.derive(productDecoder, null)

  given productDecoder[T](using inst: K0.ProductInstances[Decoder, T], typeable: Typeable[T]) as Decoder[T] = new Decoder[T] {
    def decode(str: String): Either[ParseError, T] = {
      type Acc = (List[String], Option[ParseError])
      inst.unfold[Decoder, T, Acc](str.split(',').toList, None)([t] => (acc: Acc, dec: Decoder[t]) => 
        acc._1 match {
          case head :: tail => dec.decode(head) match {
            case Right(t) => ((tail, None), Some(t))
            case Left(e)  => ((Nil, Some(e)), None)
          }
          case Nil => (acc, None)
        }
      ) match {
        case ((_, Some(e)), None) => Left(e)
        case ((_, None), None)    => Left(ParseError(str, s"value is not valid ${typeable.describe}"))
        case (_, Some(t))         => Right(t)
      }
    }
  }
}

case class A(i: Int, s: String) derives Decoder

@main def test = {
  println(summon[Decoder[A]].decode("10,abc")) //Right(A(10,abc))
  println(summon[Decoder[A]].decode("xxx,abc")) //Left(ParseError(xxx,value is not valid Int))
  println(summon[Decoder[A]].decode(",")) //Left(ParseError(,,value is not valid A))
}

build.sbt

scalaVersion := "0.24.0"

libraryDependencies += "org.typelevel" %% "shapeless-core" % "3.0.0-M1"

project/plugins.sbt

addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.1")


使用 Dotty宏 + 使用类型类中最低的子类型?

I am trying to learn meta-programming in dotty. Specifically compile time code generation. I thought learning by building something would be a good approach. So I decided to make a CSV parser which will parse lines into case classes. I want to use dotty macros to generate decoders

trait Decoder[T]{
  def decode(str:String):Either[ParseError, T]
}

object Decoder {
  inline given stringDec as Decoder[String] = new Decoder[String] {
    override def decode(str: String): Either[ParseError, String] = Right(str)
  }

  inline given intDec as Decoder[Int] = new Decoder[Int] {
    override def decode(str: String): Either[ParseError, Int] =
      str.toIntOption.toRight(ParseError(str, "value is not valid Int"))
  }
  
  inline def forType[T]:Decoder[T] = ${make[T]}

  def make[T:Type](using qctx: QuoteContext):Expr[Decoder[T]] = ???
}

I have provided basic decoders for Int & String, now I looking for guidance for def make[T:Type] method. How to iterate parameter list of a case class T inside this method? Are there any recommended ways or patterns to do this?

解决方案

Using standard type class derivation in Dotty

import scala.deriving._
import scala.compiletime._

case class ParseError(str: String, msg: String)
  
trait Decoder[T]{
  def decode(str:String): Either[ParseError, T]
}

object Decoder {
  inline given stringDec as Decoder[String] = new Decoder[String] {
    override def decode(str: String): Either[ParseError, String] = Right(str)
  }

  inline given intDec as Decoder[Int] = new Decoder[Int] {
    override def decode(str: String): Either[ParseError, Int] =
      str.toIntOption.toRight(ParseError(str, "value is not valid Int"))
  }

  inline derived[T](using m: Mirror.Of[T]): Decoder[T] = {
    val elemInstances = summonAll[m.MirroredElemTypes]
    inline m match {
      case p: Mirror.ProductOf[T] => productDecoder(p, elemInstances)
      case s: Mirror.SumOf[T]     => ???
    }
  }

  inline def summonAll[T <: Tuple]: List[Decoder[_]] = inline erasedValue[T] match {
    case _: Unit /* EmptyTuple in 0.25 */ => Nil
    case _: (t *: ts) => summonInline[Decoder[t]] :: summonAll[ts]
  }

  def productDecoder[T](p: Mirror.ProductOf[T], elems: List[Decoder[_]]): Decoder[T] =
    new Decoder[T] {
      def decode(str: String): Either[ParseError, T] = {
        elems.zip(str.split(','))
          .map(_.decode(_).map(_.asInstanceOf[AnyRef]))
          .sequence
          .map(ts => p.fromProduct(new ArrayProduct(ts.toArray)))
      }
    }

  def [E,A](es: List[Either[E,A]]) sequence: Either[E,List[A]] =
    traverse(es)(x => x)

  def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
    es.foldRight[Either[E, List[B]]](Right(Nil))((h, tRes) => map2(f(h), tRes)(_ :: _))

  def map2[E, A, B, C](a: Either[E, A], b: Either[E, B])(f: (A, B) => C): Either[E, C] = 
    for { a1 <- a; b1 <- b } yield f(a1,b1)
}

case class A(i: Int, s: String) derives Decoder
  
@main def test = {
  println(summon[Decoder[A]].decode("10,abc"))//Right(A(10,abc))
  println(summon[Decoder[A]].decode("xxx,abc"))//Left(ParseError(xxx,value is not valid Int))
  // println(summon[Decoder[A]].decode(","))
}

Tested in 0.24.0.


Using Shapeless-3

import shapeless.{K0, Typeable}

case class ParseError(str: String, msg: String)

trait Decoder[T]{
  def decode(str:String): Either[ParseError, T]
}

object Decoder {
  inline given stringDec as Decoder[String] = new Decoder[String] {
    override def decode(str: String): Either[ParseError, String] = Right(str)
  }

  inline given intDec as Decoder[Int] = new Decoder[Int] {
    override def decode(str: String): Either[ParseError, Int] =
      str.toIntOption.toRight(ParseError(str, "value is not valid Int"))
  }

  inline def derived[A](using gen: K0.Generic[A]): Decoder[A] =
    gen.derive(productDecoder, null)

  given productDecoder[T](using inst: K0.ProductInstances[Decoder, T], typeable: Typeable[T]) as Decoder[T] = new Decoder[T] {
    def decode(str: String): Either[ParseError, T] = {
      type Acc = (List[String], Option[ParseError])
      inst.unfold[Decoder, T, Acc](str.split(',').toList, None)([t] => (acc: Acc, dec: Decoder[t]) => 
        acc._1 match {
          case head :: tail => dec.decode(head) match {
            case Right(t) => ((tail, None), Some(t))
            case Left(e)  => ((Nil, Some(e)), None)
          }
          case Nil => (acc, None)
        }
      ) match {
        case ((_, Some(e)), None) => Left(e)
        case ((_, None), None)    => Left(ParseError(str, s"value is not valid ${typeable.describe}"))
        case (_, Some(t))         => Right(t)
      }
    }
  }
}

case class A(i: Int, s: String) derives Decoder

@main def test = {
  println(summon[Decoder[A]].decode("10,abc")) //Right(A(10,abc))
  println(summon[Decoder[A]].decode("xxx,abc")) //Left(ParseError(xxx,value is not valid Int))
  println(summon[Decoder[A]].decode(",")) //Left(ParseError(,,value is not valid A))
}

build.sbt

scalaVersion := "0.24.0"

libraryDependencies += "org.typelevel" %% "shapeless-core" % "3.0.0-M1"

project/plugins.sbt

addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.1")


Using Dotty macros + TASTy reflection like in dotty-macro-examples/macroTypeclassDerivation (this approach is even more low-level than the one with scala.deriving.Mirror)

import scala.quoted._
  
case class ParseError(str: String, msg: String)

trait Decoder[T]{
  def decode(str:String): Either[ParseError, T]
}

object Decoder {
  inline given stringDec as Decoder[String] = new Decoder[String] {
    override def decode(str: String): Either[ParseError, String] = Right(str)
  }

  inline given intDec as Decoder[Int] = new Decoder[Int] {
    override def decode(str: String): Either[ParseError, Int] =
      str.toIntOption.toRight(ParseError(str, "value is not valid Int"))
  }

  inline def derived[T]: Decoder[T] = ${ derivedImpl[T] }

  def derivedImpl[T](using qctx: QuoteContext, tpe: Type[T]): Expr[Decoder[T]] = {
    import qctx.tasty._
    val tpeSym = tpe.unseal.symbol
    if (tpeSym.flags.is(Flags.Case)) productDecoder[T]
    else if (tpeSym.flags.is(Flags.Trait & Flags.Sealed)) ???
    else sys.error(s"Unsupported combination of flags: ${tpeSym.flags.show}")
  }

  def productDecoder[T](using qctx: QuoteContext, tpe: Type[T]): Expr[Decoder[T]] = {
    import qctx.tasty._    
    val fields: List[Symbol]             = tpe.unseal.symbol.caseFields
    val fieldTypeTrees: List[TypeTree]   = fields.map(_.tree.asInstanceOf[ValDef].tpt)
    val fieldTypes: List[Type]           = fieldTypeTrees.map(_.tpe)
    val decoderTerms: List[Term]         = fieldTypes.map(lookupDecoderFor(_))
    val decoders: Expr[List[Decoder[_]]] = Expr.ofList(decoderTerms.map(_.seal.cast[Decoder[_]]))

    def mkT(fields: Expr[List[_]]): Expr[T] = {
      Apply(
        Select.unique(New(tpe.unseal), "<init>"),
        fieldTypeTrees.zipWithIndex.map((fieldType, i) =>
          TypeApply(
            Select.unique(
              Apply(
                Select.unique(
                  fields.unseal,
                  "apply"),
                List(Literal(Constant(i)))
              ), "asInstanceOf"),
            List(fieldType)
          )
        )
      ).seal.cast[T]
    } 
      
    '{
      new Decoder[T]{
        override def decode(str: String): Either[ParseError, T] = {
          str.split(',').toList.zip($decoders).map((str, decoder) =>
            decoder.decode(str)
          ).sequence.map(fields =>
            ${mkT('fields)}
          )
        }
      }
    }
  }

  def lookupDecoderFor(using qctx: QuoteContext)(t: qctx.tasty.Type): qctx.tasty.Term = {
    import qctx.tasty._
    val tpe = AppliedType(Type(classOf[Decoder[_]]), List(t))
    searchImplicit(tpe) match {
      case res: ImplicitSearchSuccess => res.tree
    }
  }

  def [E,A](es: List[Either[E,A]]) sequence: Either[E,List[A]] =
    traverse(es)(x => x)

  def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
    es.foldRight[Either[E, List[B]]](Right(Nil))((h, tRes) => map2(f(h), tRes)(_ :: _))

  def map2[E, A, B, C](a: Either[E, A], b: Either[E, B])(f: (A, B) => C): Either[E, C] =
    for { a1 <- a; b1 <- b } yield f(a1,b1)
}

case class A(i: Int, s: String) derives Decoder

@main def test = {
  println(summon[Decoder[A]].decode("10,abc"))//Right(A(10,abc))
  println(summon[Decoder[A]].decode("xxx,abc"))//Left(ParseError(xxx,value is not valid Int))
 // println(summon[Decoder[A]].decode(","))
}

Tested in 0.24.0.


For comparison deriving type classes in Scala 2

Use the lowest subtype in a typeclass?

这篇关于如何在Dotty宏中访问案例类的参数列表的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆