Automatic generation of type classes in Scala 3

Scala uses an extensive approach to endowing classes with additional functionality called type classes. For those who have never encountered this approach, I recommend reading this article . This approach allows you to keep the code of some aspects of the class functioning separately from the actual implementation of the class. And create it without even having access to the code of the class itself. In particular, this approach is justified and is recommended when endowing classes with the ability to serialize / deserialize into a specific format. For example, the library for working with Json from the Play framework uses type classes to set rules for representing objects in json format.





If the type class is intended to be used in a large number of different classes (such as in serialization / deserialization), then writing the type class code for each class with which it should work is irrational and laborious. In many cases, you can generate a type class implementation automatically knowing the set of attributes of the class for which it is intended. Unfortunately, in the current version of scala, automatic generation of the type class is difficult. It requires you to either write macros yourself, or use third-party frameworks to generate type classes such as shapeless or magnolia , which are also macro-based.





Scala 3, which is rapidly moving towards release, has a built-in language for automatic generation of type class. This article attempts to understand the use of this mechanism using a concrete type class as an example.





Type class declaration

As an example, we will use a rather artificial type class which we will call Inverter. It will contain one method:





trait Inverter[T] {

  def invert(value: T): T

}
      
      



"" . , - , - NOT. . type class , , .





- type class . given ( implicit Scala 2) Inverter Inverter:





object Inverter {

  given Inverter[String] = new Inverter[String] {
    override def invert(str: String): String =
      str.reverse
  }

  given Inverter[Int] = new Inverter[Int] {
    override def invert(value: Int): Int =
      -value
  }
  
  given Inverter[Boolean] = new Inverter[Boolean] {
    override def invert(value: Boolean): Boolean =
      !value
  }
  
}
      
      



Inverter . derived[T] Inverter[T]. . type class ( shapeless 3). . derived Mirror.Of[T]. . Mirror.Of[T] :





  • case case





  • (enum enum cases)





  • sealed trait- case case .





type class .





runtime , compile time Scala 3 ( , , , inline).





derived . case ( ).





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

  inline def summonAll[T <: Tuple]: List[Inverter[_]] =
    inline erasedValue[T] match
      case _: EmptyTuple => List()
      case _: (t *: ts) => summonInline[Inverter[t]] :: summonAll[ts]
      
      



. Miror.Of[T] MirroredElemTypes. case . , Inverter . summonAll. summonAll given summonInline. , summonAll type class .





Inverter - (case , case , ) (sealed trait enum). , productInverter, Inverter Inverter :





def productInverter[T](p: Mirror.ProductOf[T], elems: List[Inverter[_]]): Inverter[T] = {
    new Inverter[T] {
      def invert(value: T): T = {
        val oldValues = value.asInstanceOf[Product].productIterator
        val newValues = oldValues.zip(elems)
          .map { case (value, inverter) =>
            inverter.asInstanceOf[Inverter[Any]].invert(value)
          }
          .map(_.asInstanceOf[AnyRef])
          .toArray
        p.fromProduct(Tuple.fromArray(newValues))
      }
    }
  }
      
      



. -, . trait Product, . - Inverter Inverter. , -, . fromProduct Mirror .





derived

derived type class . . - case derives type class. :





case class Sample(intValue: Int, stringValue: String, boolValue: Boolean) derives Inverter
      
      



Inverter[Sample] Sample. summon :





println(summon[Inverter[Sample]].invert(Sample(1, "abc", false)))
// : Sample(-1,cba,true)
      
      



type class .





, type class. given derived:





case class Sample(intValue: Int, stringValue: String, boolValue: Boolean)

@main def mainProc = {
  
  given Inverter[Sample] = Inverter.derived
  println(summon[Inverter[Sample]].invert(Sample(1, "abc", false)))
  // : Sample(-1,cba,true)
  
} 
      
      



type class . case type class . :





case class InnerSample(s: String)
case class OuterSample(inner: InnerSample)
      
      



type class:





  given Inverter[InnerSample] = Inverter.derived
  given Inverter[OuterSample] = Inverter.derived
  println(summon[Inverter[OuterSample]].invert(OuterSample(InnerSample("abc"))))
  // : OuterSample(InnerSample(cba))
      
      



type class Mirror.Of. given:





  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]
  
  println(summon[Inverter[OuterSample]].invert(OuterSample(InnerSample("abc"))))
  // : OuterSample(InnerSample(cba))
      
      



type class . trait , ( import ) , :





trait AutoInverting {
  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]
}
      
      



type class

type class type class . .





case :





case class SampleUnprotected(value: String)
case class SampleProtected(value: String)
case class Sample(prot: SampleProtected, unprot: SampleUnprotected)
      
      



SampleProtected Inverter, value. type class Sample:





  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]
  
  given Inverter[SampleProtected] = new Inverter[SampleProtected] {
    override def invert(prot: SampleProtected): SampleProtected = SampleProtected(prot.value)
  }
  
  println(summon[Inverter[Sample]].invert(Sample(SampleProtected("abc"), SampleUnprotected("abc"))))
  // : Sample(SampleProtected(abc),SampleUnprotected(cba))
      
      



Inverter Sample Inverter SampleProtected. .





sealed trait enum

type class case ( ) type class sealed trait . derived , :





  inline def derived[T](using m: Mirror.Of[T]): Inverter[T] = {
    val elemInstances = summonAll[m.MirroredElemTypes]
    inline m match {
      case p: Mirror.ProductOf[T] =>
        productInverter[T](p, elemInstances, getFields[p.MirroredElemLabels])
      case s: Mirror.SumOf[T] => 
        sumInverter(s, elemInstances)
    }
  }

  def sumInverter[T](s: Mirror.SumOf[T], elems: List[Inverter[_]]): Inverter[T] = {
    new Inverter[T] {
      def invert(value: T): T = {
        val index = s.ordinal(value)
        elems(index).asInstanceOf[Inverter[Any]].invert(value).asInstanceOf[T]
      }
    }
  }
      
      



. Mirror . ordinal Mirror. . Inverter ( ) .





. sealed trait Either Option:





def checkInverter[T](value: T)(using inverter: Inverter[T]): Unit = {
  println(s"$value => ${inverter.invert(value)}")
}
  
@main def mainProc = {
  
  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]
  
  given Inverter[SampleProtected] = new Inverter[SampleProtected] {
    override def invert(prot: SampleProtected): SampleProtected = SampleProtected(prot.value)
  }
  
  val eitherSampleLeft: Either[SampleProtected, SampleUnprotected] = Left(SampleProtected("xyz"))
  checkInverter(eitherSampleLeft)
  // : Left(SampleProtected(xyz)) => Left(SampleProtected(xyz))
  val eitherSampleRight: Either[SampleProtected, SampleUnprotected] = Right(SampleUnprotected("xyz"))
  checkInverter(eitherSampleRight)
  // : Right(SampleUnprotected(xyz)) => Right(SampleUnprotected(zyx))
  val optionalValue: Option[String] = Some("123")
  checkInverter(optionalValue)
  // : Some(123) => Some(321)
  val optionalValue2: Option[String] = None
  checkInverter(optionalValue2)
  // : None => None
  checkInverter((6, "abc"))
  // : (6,abc) => (-6,cba)
}
      
      



Inverter type class summon. Either ( SumOf, ProductOf).





type class , /. , . type class . Inverter : . . derived:





  inline def derived[T](using m: Mirror.Of[T]): Inverter[T] = {
    val elemInstances = summonAll[m.MirroredElemTypes]
    inline m match {
      case p: Mirror.ProductOf[T] =>
        productInverter[T](p, elemInstances, getFields[p.MirroredElemLabels])
      case s: Mirror.SumOf[T] => 
        sumInverter(s, elemInstances)
    }
  }

  inline def getFields[Fields <: Tuple]: List[String] =
    inline erasedValue[Fields] match {
      case _: (field *: fields) => constValue[field].toString :: getFields[fields]
      case _ => List()
    }

  def productInverter[T](p: Mirror.ProductOf[T], elems: List[Inverter[_]], labels: Seq[String]): Inverter[T] = {
    new Inverter[T] {
      def invert(value: T): T = {
        val newValues = value.asInstanceOf[Product].productIterator
          .zip(elems).zip(labels)
          .map { case ((value, inverter), label) =>
            if (label.startsWith("__"))
              value
            else
              inverter.asInstanceOf[Inverter[Any]].invert(value)
          }
          .map(_.asInstanceOf[AnyRef])
          .toArray
        p.fromProduct(Tuple.fromArray(newValues))
      }
    }
  }
      
      



:





case class Sample(value: String, __hidden: String)
      
      



For such a class, value must be inverted, but __hidden must not be inverted:





  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]
  
  println(summon[Inverter[Sample]].invert(Sample("abc","abc")))
  // : Sample(cba,abc)
      
      



conclusions

As you can see, the built-in implementation of the type class generation is quite usable, quite convenient and covers the basic usage patterns. It seems to me that this mechanism will allow in most cases to do without macros and without third-party libraries for generating type class.





You can play around with the source code for the final example covered in this article .








All Articles