Higher Kinded Data
Data
FP Data Types
- Product
- Sum
- Function
- Recursion
Scala Products
case class Person(
firstName: String,
lastName: Option[String],
age: Int,
tags: List[Tag],
)
Usage
val person =
Person(
firstName = "レグ",
lastName = None,
age = 214,
tags = List("robot", "indestructible")
)
person.firstName // String
person.copy(firstName = newName) // type checking
Sudden task
Dystypia
val storedPerson: Map[String, Any] = Map(
"lastName" -> None,
"age" -> 18,
"tags" -> List(),
)
val updatedPerson: Map[String, Any] = Map(
"firstName" -> "Nadya",
"tags" -> List("starfleet", "viper")
)
val fullPerson = storedPerson ++ updatedPerson
def recreate(value: Map[String, Any]): Either[String, Person] = ???
Dystypia
val storedPerson: Map[String, Any] = Map(
"lastName" -> None,
"age" -> 18,
"tags" -> List(),
)
val updatedPerson: Map[String, Any] = Map(
"firstName" -> "Nadya",
"tags" -> List("starfleet", "viper")
)
val fullPerson = storedPerson ++ updatedPerson
def recreate(value: Map[String, Any]): Either[String, Person] = ???
Copypasta support
case class PartialPerson(
firstName: Option[String],
lastName: Option[Option[String]],
age: Option[Int],
tags: Option[List[Tag]],
) {
def toPerson: EitherNel[String, Person] = ???
}
given Monoid[PartialPerson] = ???
given Decoder[PartialPerson] = ???
given Encoder[PartialPerson] = ???
given Swagger[PartialPerson] = ???
Other painful ways
- Reflection (~ String Typing)
- Shapeless
type PartialPerson =
FieldType["firstName" , Option[String]] ::
FieldType["lastName", Option[Option[String]]] ::
FieldType["age", Option[Int]]::
FieldType["tags", Option[List[Tag]]] ::
HNil
Other tasks
case class PersonVariants(
firstName: List[String],
lastName: List[Option[String]],
age: List[Int],
tags: List[List[Tag]],
)
case class MutablePerson(
firstName: Ref[IO, String],
lastName: Ref[IO, Option[String]],
age: Ref[IO, Int],
tags: Ref[IO, List[Tag]],
)
case class PersonChanges(
firstName: Stream[IO, String],
lastName: Stream[IO, Option[String]],
age: Stream[IO, Int],
tags: Stream[IO, List[Tag]],
)
type Listen[-A] = (A, A) => IO[Unit]
case class PersonListen(
firstName: Listen[String],
lastName: Listen[Option[String]],
age: Listen[Int],
tags: Listen[List[Tag]],
)
Higher-Kinded Data
case class PersonOf[F[_]](
firstName: F[String],
lastName: F[Option[String]],
age: F[Int],
tags: F[List[Tag]],
)
Higher-Kinded Data
case class PersonOf[F[_]](
firstName: F[String],
lastName: F[Option[String]],
age: F[Int],
tags: F[List[Tag]],
)
type Person = PersonOf[Id] // PersonOf[[A] =>> A]
type PartialPerson = PersonOf[Option]
type PersonVariants = PersonOf[List]
type MutablePerson[F[_]] = PersonOf[Ref[F, *]]
type PersonChanges[F[_]] = PersonOf[Stream[F, *]]
type PersonListen[F[_]] = PersonOf[Listen]
Names
type Name[A] = String
type Names[Data[f[_]]] = Data[Name]
given Names[PersonOf] = PersonOf(
firstName = "firstName",
lastName = "lastName",
age = "age",
tags = "tags"
)
inline def names[Data[f[_]]](using p: Mirror.ProductOf[Data[Name]]) =
p.fromProduct(constValueTuple[p.MirroredElemLabels])
Typeclass provision
type Provided[Data[f[_]], TC[_]] = Data[TC]
given PersonOf[Show] = PersonOf(
implicitly,
implicitly,
implicitly,
implicitly,
)
inline def provision[P](using p: Mirror.ProductOf[P]): P =
p.fromProduct(summonAll[p.MirroredElemTypes])
Functor
trait Functor[F[_]]:
extension [A, B](fa: F[A])
def map(f: A => B): F[B]
case class Person[R](
firstName: R,
lastName: R,
age: R,
tags: R
)
given Functor[Person] with
extension [A, B](fa: Person[A])
def map(f: A => B): Person[B] =
Person(
f(fa.firstName),
f(fa.lastName),
f(fa.age),
f(fa.tags),
)
Pure / Pointed
trait Pure[F[_]]:
def pure[A](a: A): F[A]
case class Person[R](
firstName: R,
lastName: R,
age: R,
tags: R
)
given Pure[Person] with
def pure[A](a: A): Person[A] =
Person(
a,
a,
a,
a
)
Apply / Semigroupal
trait Apply[F[_]] extends Functor[F]:
extension [A, B, C](fa: F[A])
def map2(fb: F[B])(f: (A, B) => C): F[C]
case class Person[R](
firstName: R,
lastName: R,
age: R,
tags: R
)
given Apply[Person] with
extension [A, B, C](fa: Person[A])
def map2(fb: Person[B])(f: (A, B) => C): Person[C] =
Person(
f(fa.firstName, fb.firstName),
f(fa.lastName, fb.lastName),
f(fa.age, fb.age),
f(fa.tags, fb.tags)
)
Applicative / Monoidal
trait Applicative[F[_]] extends Pure[F] with Apply[F]:
extension [A, B] (fa: F[A]) override def map (f: A => B): F[B] =
fa.map2(unit)((a, _) => f(a))
def zip[A, B](fa: F[A], fb: F[B]): F[(A, B)] = fa.map2(fb)((_, _))
extension [A, B, C, D](fa: F[A])
def map3(fb: F[B], fc: F[C])(f: (A, B, C) => D): F[D] =
zip(fa, fb).map2(fc) {
case ((a, b), c) => f(a, b, c)
}
extension [A, B, C, D, E](fa: F[A])
def map4(fb: F[B], fc: F[C], fd: F[D])(f: (A, B, C, D) => E): F[E] =
zip(fa, fb).map2(zip(fc, fd)){
case ((a, b), (c, d)) => f(a, b, c, d)
}
Traverse
trait Traverse[T[_]] extends Functor[T]:
extension [A, B, F[_]: Applicative](ta: T[A])
def traverse(f: A => F[B]): F[T[B]]
extension [A, F[+_]: Applicative](ta: T[F[A]])
def sequence: F[T[A]] = ta.traverse(fa => fa)
given Traverse[Person] with
extension [A, B, F[_]](ta: Person[A])
def traverse(f: A => F[B])(using F: Applicative[F]): F[Person[B]] =
f(ta.firstName).map4(
f(ta.lastName),
f(ta.age),
f(ta.tags)
)(Person(_, _, _, _))
Transformation
trait FunctionK[F[_], G[_]]:
def apply[A](fa: F[A]): G[A]
type FunctionK[F[_], G[_]] = [A] => F[A] => G[A]
FunctorK
trait FunctorK[U[f[_]]]:
extension [F[_], G[_]] (obj: U[F])
def mapK (f: [A] => F[A] => G[A]): U[G]
case class PersonOf[F[_]](
firstName: F[String],
lastName: F[Option[String]],
age: F[Int],
tags: F[List[Tag]],
)
given FunctorK[PersonOf] with
extension [F[_], G[_]] (p: PersonOf[F])
def mapK (f: [A] => F[A] => G[A]): PersonOf[G] =
PersonOf(
f(p.firstName),// F[String] -> G[String]
f(p.lastName), // F[Option[String]]->G[Option[String]]
f(p.age), // F[Int] -> G[Int]
f(p.tags), // F[List[Tag]] -> G[List[Tag]]
)
PureK / PointedK
trait PureK[U[f[_]]] extends FunctorK[U]:
def pureK[F[_]](gen: [A] => () => F[A]): U[F]
case class PersonOf[F[_]](
firstName: F[String],
lastName: F[Option[String]],
age: F[Int],
tags: F[List[Tag]],
)
given PureK[PersonOf] with
def pureK[F[_]](gen: [A] => () => F[A]): PersonOf[F] =
PersonOf(
gen(), // F[String]
gen(), // F[Option[String]]
gen(), // F[Int]
gen(), // F[List[Tag]]
)
ApplyK / SemigroupalK
trait ApplyK[U[f[_]]] extends FunctorK[U]:
extension [F[_], G[_], H[_]] (left: U[F])
def map2K(right: U[G])(f: [A] => (F[A], G[A]) => H[A]): U[H]
case class PersonOf[F[_]](
firstName: F[String],
lastName: F[Option[String]],
age: F[Int],
tags: F[List[Tag]],
)
given ApplyK[PersonOf] with
extension [F[_], G[_], H[_]] (left: PersonOf[F])
def map2K(right: PersonOf[G])
(f: [A] => (F[A], G[A]) => H[A]): PersonOf[H] =
PersonOf(
f(left.firstName, right.firstName),
// F[String] G[String] => H[String]
f(left.lastName, right.lastName), // H[Option[String]]
f(left.age, right.age), // H[Int]
f(left.tags, right.tags) // H[List[Tag]]
)
ApplicativeK / MonoidalK
trait ApplicativeK[U[f[_]]] extends ApplyK[U] with PureK[U]:
extension [F[_], G[_]] (obj: U[F])
override def mapK(f: [A] => F[A] => G[A]): U[G] =
obj.map2K[F, [A] =>> Unit, G](
pureK([A] => () => ()))(
[A] => (x: F[A], y: Unit) => f(x))
extension [F[_], G[_], H[_]] (left: U[F])
def map2Given(right: U[G])(
f: [A] => (F[A]) => G[A] ?=> H[A]): U[H] =
left.map2K(right)([A] => (fa: F[A], ga: G[A]) => {
given G[A] = ga
f(fa)
})
TraverseK
trait TraverseK[U[f[_]]] extends FunctorK[U]:
extension[F[_], G[+_], H[_]](uf: U[F])
def traverseK(f: [A] => F[A] => G[H[A]])(
using Applicative[G]): G[U[H]]
given TraverseK[PersonOf] with
extension[F[_], G[+_], H[_]](uf: PersonOf[F])
def traverseK(f: [A] => F[A] => G[H[A]])(
using Applicative[G]): G[PersonOf[H]] =
f(uf.firstName).map4(
f(uf.lastName),
f(uf.age),
f(uf.tags)
)(PersonOf(_, _, _, _))
HKD
type Rep[-U[f[_]], A] = [F[_]] => U[F] => F[A]
trait Craft[U[f[_]]]
extends RepresentableK[U]
with TraverseK[U]:
def craft[F[+_]: Applicative, G[_]](
gain: [A] => Rep[U, A] => F[G[A]]
): F[U[G]]
object Craft:
inline def derived[U[f[_]] <: Product]
: Craft[U] = deriveCraft
case class PersonOf[F[_]](...)
derives Craft
Auto Monoids
given [Data[f[_]]](using HKD: Craft[Data]): Monoid[Data[Option]] with
val empty = HKD.pureK([A] => () => None)
def combine(x: Data[Option], y: Data[Option]): Data[Option] =
x.map2K(y)([A] => (xo: Option[A], yo: Option[A]) => yo.orElse(xo))
Combine and Traverse
extension [Data[f[_]]: Craft] (xs: Data[Option])(using names: Data[Name])
def build: EitherNel[String, Data[[A] =>> A]] =
names.map2K[H = [A] =>> EitherNel[String, A]](xs)(
[A] => (name: String, x: Option[A]) =>
x.toRightNel(name)
).parSequenceId
trait TraversableK[U[f[_]]] extends FunctorK[U]:
extension[F[_]](uf: U[F])
def parSequenceId(using Parallel[F]): F[U[Id]] = ...
Example
@main def personParts =
val init = Monoid.empty[PersonOf[Option]]
val p1 = init.copy(
firstName = Some("Oleg"),
lastName = Some(None),
)
val p2 = init.copy(
age = Some(35),
tags = Some(List("scala", "love"))
)
println(init.build)
println(p1.build)
println(p2.build)
println((p1 |+| p2).build)
[info] running hkd.personParts
Left(NonEmptyList(firstName, lastName, age, tags))
Left(NonEmptyList(age, tags))
Left(NonEmptyList(firstName, lastName))
Right(PersonOf(Oleg,None,35,List(scala, love)))
Extracting Collection
opaque type Const[X, +A] = X
object Const:
def apply[X](x: X): Const[X, Nothing] = x
given [X: Monoid] : Applicative[[a] =>> Const[X, a]] with
def pure[A](x: A) = Monoid.empty[X]
extension [A, B] (xa: Const[X, A])
override def map(f: A => B): Const[X, B] = xa
extension [A, B, C](xa: Const[X, A])
def map2(xb: Const[X, B])(f: (A, B) => C) = xa |+| xb
extension [A] (x: Const[A, Any]) def value: A = x
def collectVector[G[+_], Data[f[_]]](xs: Data[G])
(using TraversableK[Data]): Vector[G[Any]] =
xs.traverseK(
[A] => (x: G[A]) => Const[Vector[G[Any]]](Vector(x))
).value
Quantified Constraints
trait ShowK[F[_]]:
given showK[A: Show]: Show[F[A]]
object ShowK:
given ShowK[Identity] with
def showK[A: Show] = summon
given ShowK[Option] with
def showK[A: Show] = implicitly
given [A: Show] : ShowK[[B] =>> Either[A, B]] with
def showK[B: Show] = implicitly
trait EqK[F[_]]:
given eqK[A: Eq]: Eq[F[A]]
trait EncoderK[F[_]]:
given encoderK[A: Encoder]: Encoder[F[A]]
trait ConstraintK[F[_], TC[_]]:
given instance[A : TC]: TC[F[A]]
Auto Show
given [Data[f[_]] <: Product: Craft, F[_]]( using
names: Data[Name],
shows: Data[Show],
showK: ShowK[F]): Show[Data[F]] with
def show(xs: Data[F]): String =
import showK.given
val fields: Data[[A] =>> String] =
xs.map2Given(shows)(
[A] => (fa: F[A]) => (x: Show[A]) ?=> fa.show)
val prefix = xs.productPrefix
val pairs: Data[[A] =>> String] =
names.map2K(fields)(
[A] => (name: String, fld: String) => s"$name: $fld")
hkdToVector(pairs).mkString(s"$prefix{", ",", "}")
How Does It Look Like
case class PersonOf[F[_]](
firstName: F[String],
lastName: F[Option[String]],
age: F[Int],
tags: F[List[Tag]],
) derives Craft
given PersonOf[Name] = names[PersonOf]
given PersonOf[Show] = provision[PersonOf[Show]]
@main def personMain =
val person = PersonOf[Id](
"Oleg",
Some("Nizhnik"),
36,
List("scala", "love")
)
println(person.show)
[info] running personMain
PersonOf{firstName: Oleg,lastName: Some(Nizhnik),age: 36,tags: List(scala, love)}
HKD
-
Infinite Utilitary Data Shapes
-
Record-like Collections
-
Type-safe typeclass instance derivation
Workarounds /
Beyond Product
Recursion
case class Person[F[_]](
... ,
children F[List[Person[Id]]],
workplace: F[Option[Workplace[Id]]],
tastes: F[Genre => Double],
)
Sum \ ADT \ Enum
stay tuned....
Questions?
HKD
By Oleg Nizhnik
HKD
- 1,270