Skip to content

Instantly share code, notes, and snippets.

@TonioGela
Last active May 6, 2024 09:38
Show Gist options
  • Save TonioGela/62e9572211ea8cf1f0bbf5da3e0689c8 to your computer and use it in GitHub Desktop.
Save TonioGela/62e9572211ea8cf1f0bbf5da3e0689c8 to your computer and use it in GitHub Desktop.
Autoderivation of typeclasses in Scala 3
//> using scala 3.4.0
//> using dep com.norbitltd::spoiwo::2.2.1
//> using dep org.typelevel::cats-core::2.10.0
//> using dep com.softwaremill.magnolia1_3::magnolia::1.3.4
import spoiwo.model.*
import spoiwo.natures.xlsx.Model2XlsxConversions.XlsxSheet
import cats.kernel.Monoid
import cats.syntax.all.*
import scala.compiletime.*
import scala.deriving.Mirror
import Excelable.*
import Excelable.given
import Excellable2.given
trait Exceller[T] {
def toRow(t:T): Row
}
trait Excelable[T] extends Exceller[T] {
def sheetName: String
def headerRow: Row
}
object Excelable {
inline def labelFromMirror[A](using m: Mirror.Of[A]): String = constValue[m.MirroredLabel]
private inline def getElemLabels[A <: Tuple]: List[String] = inline erasedValue[A] match
case _: EmptyTuple => Nil
case _: (head *: tail) => constValue[head].toString :: getElemLabels[tail]
private inline def getElemLabelsHelper[A](using m: Mirror.Of[A]): List[String] = getElemLabels[m.MirroredElemLabels]
private inline def getTypeclassInstances[A <: Tuple]: List[Exceller[Any]] = inline erasedValue[A] match
case _: EmptyTuple => Nil
case _: (head *: tail) => summonInline[Exceller[head]].asInstanceOf[Exceller[Any]] :: getTypeclassInstances[tail]
private inline def summonInstancesHelper[A](using m: Mirror.Of[A]) = getTypeclassInstances[m.MirroredElemTypes]
given Monoid[Row] = new Monoid[Row] {
override def combine(x: Row, y: Row): Row = Row(x.cells.concat(y.cells))
override def empty: Row = Row()
}
private inline def deriveExcelableCaseClass[A](using m: Mirror.ProductOf[A]) =
new Excelable[A] {
val elemInstances = getTypeclassInstances[m.MirroredElemTypes]
override def sheetName: String = s"${labelFromMirror[A]}s"
override def headerRow: Row = {
val labels = getElemLabels[m.MirroredElemLabels].zip(elemInstances).map {
case (_, instance: Excelable[Any]) => instance.headerRow
case (label, _) => Row(Cell(label))
}
labels.combineAll
}
override def toRow(a: A): Row =
val elems = a.asInstanceOf[Product].productIterator
elemInstances.zip(elems).map(_.toRow(_)).combineAllOption.getOrElse(
Row(Cell(labelFromMirror[A]))
)
}
private inline def deriveExcelableADT[A](using m: Mirror.SumOf[A]) =
new Excelable[A] {
override def sheetName: String = s"${labelFromMirror[A]}s"
override def headerRow: Row = Row(Cell(labelFromMirror[A]))
override def toRow(a: A): Row =
val elemInstances = getTypeclassInstances[m.MirroredElemTypes]
elemInstances(m.ordinal(a)).toRow(a)
}
inline given derived[A](using m: Mirror.Of[A]): Excelable[A] = summonFrom {
case s: Mirror.SumOf[A] => deriveExcelableADT(using s)
case p: Mirror.ProductOf[A] => deriveExcelableCaseClass(using p)
}
def apply[A:Excelable]: Excelable[A] = summon
extension [A: Excelable](list: List[A])
def toSheet: Sheet = Sheet(
name = Excelable[A].sheetName,
rows = Excelable[A].headerRow :: list.map(Excelable[A].toRow)
)
}
given Exceller[String] = s => Row(Cell(s))
given Exceller[Int] = i => Row(Cell(i))
//!--------------------------------
enum UserRole derives Excellable2:
case Boss, Worker
case class UserInfo(age: Int, work:String, role: UserRole) derives Excellable2
case class User(name: String, surname: String, info: UserInfo) derives Excellable2
import Excellable2.*
@main def main =
List(
User("Tonio", "Gela", UserInfo(34, "programme", UserRole.Boss)),
User("Galileo", "Galilei", UserInfo(60, "genius", UserRole.Worker))
).toSheet2.saveAsXlsx("hello.xlsx")
// List(
// User("Tonio", "Gela", UserInfo(34, "programme", UserRole.Boss)),
// User("Galileo", "Galilei", UserInfo(60, "genius", UserRole.Worker))
// ).toSheet.saveAsXlsx("hello.xlsx")
import magnolia1.*
trait Excellable2[T] extends Exceller[T] {
def sheetName: String
def headerRow: Row
}
object Excellable2 extends AutoDerivation[Excellable2]:
def apply[T: Excellable2]: Excellable2[T] = summon
def join[T](ctx: CaseClass[Excellable2, T]): Excellable2[T] = new Excellable2[T] {
override def sheetName: String = s"${ctx.typeInfo.short}s"
override def headerRow: Row = ctx.parameters.map { parameter =>
parameter.typeclass match {
case i: Excelable[?] => i.headerRow
case _ => Row(Cell(parameter.label))
}
}.toList.combineAll
override def toRow(t: T): Row = ctx.parameters.map { parameter =>
parameter.typeclass.toRow(parameter.deref(t))
}.toList.combineAllOption.getOrElse(Row(Cell(ctx.typeInfo.short)))
}
override def split[T](ctx: SealedTrait[Excellable2, T]): Excellable2[T] = new Excellable2[T] {
override def sheetName: String = s"${ctx.typeInfo.short}s"
override def headerRow: Row = Row(Cell(ctx.typeInfo.short))
override def toRow(t: T): Row = ctx.choose(t) { sub => sub.typeclass.toRow(sub.value) }
}
extension [A: Excellable2](list: List[A])
def toSheet2: Sheet = Sheet(
name = Excellable2[A].sheetName,
rows = Excellable2[A].headerRow :: list.map(Excellable2[A].toRow)
)
given Excellable2[String] = new Excellable2[String] {
def sheetName = "Strings"
def headerRow = Row(Cell("string"))
def toRow(s:String) = Row(Cell(s))
}
given Excellable2[Int] = new Excellable2[Int] {
def sheetName = "Ints"
def headerRow = Row(Cell("int"))
def toRow(i:Int) = Row(Cell(i))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment