= ByteVector(20 bytes, 0x00112233445566778899aabbccddeeff00112233) val y = bin"1000101011011000100101" // y: BitVector = BitVector(22 bits, 0x8ad894) val z = hex"Cow!" // foo.scala: hexadecimal string literal may only contain characters [0-9a-fA-f] // val z = hex"Cow!" // ^ // Compilation Failed
~ cstring // a: Codec[((Int, Boolean), String)] = … val b = int8 ~~ bool ~~ cstring // b: Codec[(Int, Boolean, String)] = … val c = int8 :: bool :: cstring // val c: Codec[Int :: Boolean :: String :: HNil] = … Scala 2 Lots of ways to do roughly the same thing Combinators have to pick which to support (flatZip vs flatPrepend) In practice, HList variant is the most common ☹
:: cstring // val a: Codec[(Int, Boolean, String)] = … val b = int64 :: a // val b: Codec[(Long, Int, Boolean, String)] = … Scala 3 Unify all of these APIs in to a single one that creates tuples of expected arity
Codec? int8 :: (bool :: cstring) Codec[(Boolean, String)] Codec[Int] Codec[(Int, Boolean, String)] We need two operations: - for all A, B <: Tuple: (Codec[A], Codec[B]) => Codec[A *: B] - for all A, B: (Codec[A], Codec[B]) => Codec[(A, B)]
bool :: ignore(2) :: cstring // val a: Codec[(Unit, Int, Boolean, Unit, String)] = … These unit values are annoying Have to manually insert them when encoding and remove them when decoding ☹
bool :: ignore(2) :: cstring // val a: Codec[(Unit, Int, Boolean, Unit, String)] = … val b = a.dropUnits // val b: Codec[(Int, Boolean, String)] = … What’s the signature of dropUnits? How do we write "the tuple you get when you remove all units from A"?
<: Tuple](codecA: Codec[A]) { inline def dropUnits: Codec[DropUnits[A]] = codecA.xmap(a => DropUnits.drop(a), b => DropUnits.insert(b)) } } type DropUnits[A <: Tuple] <: Tuple = A match { case hd *: tl => hd match { case Unit => DropUnits[tl] case _ => hd *: DropUnits[tl] } case EmptyTuple => EmptyTuple }
= A match { case hd *: tl => hd match { case Unit => DropUnits[tl] case _ => hd *: DropUnits[tl] } case EmptyTuple => EmptyTuple } DropUnits is a match type - Defined by pattern matching on types - Supports recursion - Defined inductively with: - Inductive case for non-empty tuple - Base case for empty tuple
:: bool :: cstring // val a: Codec[(Int, Boolean, String)] = … case class Foo(x: Int, y: Boolean, z: String) val b = a.as[Foo] // val b: Codec[Foo] = …
Decoder[A] { def as[B](using iso: Iso[A, B]): Codec[B] = xmap(iso.to, iso.from) } trait Iso[A, B] { def to(a: A): B def from(b: B): A } How can we create an Iso instance for a case class and a tuple of its elements? ?
m: Mirror.ProductOf[P] { type MirroredElemTypes = T }) as Iso[T, P] = instance[T, P](fromTuple)(toTuple) def fromTuple[A, B <: Tuple](b: B)(using m: Mirror.ProductOf[A] { type MirroredElemTypes = B }): A = m.fromProduct(b.asInstanceOf[Product]).asInstanceOf[A] def toTuple[A, B <: Tuple](a: A)(using m: Mirror.ProductOf[A] { type MirroredElemTypes = B }): B = Tuple.fromProduct(a.asInstanceOf[Product]).asInstanceOf[B]
Mirror.Of[A]): Codec[A] = new Codec[A] { def sizeBound = inline m match { case p: Mirror.ProductOf[A] => sizeBoundElems[p.MirroredElemTypes] case s: Mirror.SumOf[A] => codecs.uint8.sizeBound + sizeBoundCases[s.MirroredElemTypes] } def encode(a: A) = ??? def decode(b: BitVector) = ??? } } Pattern match on the mirror of the type param, providing an implementation for both products (case classes) and sums (enums/ADTs) 2
scodec without sacrificing expressiveness • Many more language features and simplifications Library Availability Increasing • munit, scalatest, scalacheck, shapeless • fastparse, sourcecode, upickle, utest • circe, cats, cats-effect, fs2 (soon) • Lots more! Language Still Evolving • Add your project to the community build • Dotty contributors get better feedback about how their changes are impacting ecosystem • Library developers get help with upgrading to newer versions