Last active
May 6, 2024 09:38
-
-
Save TonioGela/62e9572211ea8cf1f0bbf5da3e0689c8 to your computer and use it in GitHub Desktop.
Autoderivation of typeclasses in Scala 3
This file contains hidden or 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
//> 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