Skip to content

Instantly share code, notes, and snippets.

@Baccata
Last active October 24, 2022 09:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Baccata/0553c85074cf7f68ad715a38b5508ba5 to your computer and use it in GitHub Desktop.
Save Baccata/0553c85074cf7f68ad715a38b5508ba5 to your computer and use it in GitHub Desktop.
Playing with kind-polymorphism to create a poly-kinded mapper construct.
//> using lib "org.typelevel::cats-core:2.8.0"
package foo
import cats.Functor
import cats.arrow.FunctionK
import cats.Id
import cats.ApplicativeError
import cats.MonadError
import scala.util.Try
import cats.syntax.all._
import cats.data.OptionT
import cats.Applicative
object Test extends App {
//############# LIBRARY CODE #############
implicit class TransformOps[FA](private val fa: FA) extends AnyVal {
def transform[Target <: AnyKind]: PartiallyAppliedMapper[FA, Target] = new PartiallyAppliedMapper[FA, Target](fa: FA)
}
class PartiallyAppliedMapper[FA, Target <: AnyKind](fa: FA) {
def apply[FK, FB](fk: FK)(implicit
mapper: HFunction[FA, FK, FB, Target]
) = mapper(fa, fk)
}
trait FunctorK[Alg[_[_]]] {
def mapK[F[_], G[_]](alg: Alg[F], fk: FunctionK[F, G]): Alg[G]
}
trait FunctionK2[F[_, _], G[_, _]]{
def apply[A, B](fa: F[A, B]) : G[A, B]
}
trait FunctorK2[Alg[_[_, _]]] {
def mapK2[F[_, _], G[_, _]](alg: Alg[F], fk: FunctionK2[F, G]): Alg[G]
}
trait HFunction[FA, FK, FB, F <: AnyKind] {
def apply(fa: FA, fk: FK): FB
}
type MonadicLift[F[_]] = [E, A] =>> F[A]
type MonadicProjection[Alg[_[_, _]], F[_]] = Alg[[E, A] =>> F[A]]
object HFunction {
implicit def function_element[A, B]: HFunction[A, A => B, B, B] =
new HFunction[A, A => B, B, B] {
def apply(fa: A, f: A => B): B =
f(fa)
}
implicit def function_functor[F[_]: Functor, A, B]
: HFunction[F[A], A => B, F[B], B] =
new HFunction[F[A], A => B, F[B], B] {
def apply(fa: F[A], f: A => B): F[B] =
Functor[F].map(fa)(f)
}
implicit def functionK_functor[F[_], G[_], A]
: HFunction[F[A], FunctionK[F, G], G[A], G] =
new HFunction[F[A], FunctionK[F, G], G[A], G] {
def apply(fa: F[A], fk: FunctionK[F, G]): G[A] =
fk(fa)
}
implicit def functionK_functorK[Alg[_[_]]: FunctorK, F[_], G[_]]
: HFunction[Alg[F], FunctionK[F, G], Alg[G], G] =
new HFunction[Alg[F], FunctionK[F, G], Alg[G], G] {
def apply(fa: Alg[F], fk: FunctionK[F, G]): Alg[G] =
implicitly[FunctorK[Alg]].mapK(fa, fk)
}
implicit def functionK2_functorK2[Alg[_[_, _]]: FunctorK2, F[_, _], G[_, _]]
: HFunction[Alg[F], FunctionK2[F, G], Alg[G], G] =
new HFunction[Alg[F], FunctionK2[F, G], Alg[G], G] {
def apply(fa: Alg[F], fk: FunctionK2[F, G]): Alg[G] =
implicitly[FunctorK2[Alg]].mapK2(fa, fk)
}
implicit def functionK_functorK2[Alg[_[_, _]]: FunctorK2, F[_], G[_]]
: HFunction[MonadicProjection[Alg, F], FunctionK[F, G], MonadicProjection[Alg, G], G] =
new HFunction[MonadicProjection[Alg, F], FunctionK[F, G], MonadicProjection[Alg, G], G] {
def apply(fa: MonadicProjection[Alg, F], fk: FunctionK[F, G]): MonadicProjection[Alg, G] =
implicitly[FunctorK2[Alg]].mapK2(fa, new FunctionK2[MonadicLift[F], MonadicLift[G]]{
def apply[E, A](fa: F[A]): G[A] = fk(fa)
})
}
}
//############# TRANSFORMATIONS #############
object transformations {
def adaptError[F[_], E](
f: PartialFunction[E, E]
)(implicit F: MonadError[F, E]): FunctionK[F, F] = new FunctionK[F, F] {
def apply[A](fa: F[A]): F[A] = F.adaptError(fa)(f)
}
def toOptionT[F[_] : Applicative] : FunctionK[Option, [A] =>> OptionT[F, A]] =
new FunctionK[Option, [A] =>> OptionT[F, A]]{
def apply[A](fa: Option[A]): OptionT[F, A] = OptionT.fromOption[F](fa)
}
val toList = new FunctionK[Option, List] {
def apply[A](fa: Option[A]): List[A] = fa.toList
}
val swap = new FunctionK2[Either, [A, B] =>> Either[B, A]]{
def apply[A, B](fa: Either[A, B]): Either[B, A] = fa.swap
}
}
//############# TESTS #############
val inc = (_: Int) + 1
println(1.transform(inc))
println(List(1, 2, 3).transform[Int](inc))
println(1.some.transform[Int](inc))
println(none[Int].transform[Int](inc))
trait Foo[F[_]] {
def foo(a: Int): F[Int]
}
object Foo {
implicit val fooFunctorK: FunctorK[Foo] = new FunctorK[Foo] {
def mapK[F[_], G[_]](alg: Foo[F], fk: FunctionK[F, G]): Foo[G] =
new Foo[G] {
def foo(a: Int): G[Int] = fk(alg.foo(a))
}
}
}
type Result[A] = Either[String, A]
val fooOpt: Foo[Option] = new Foo[Option] {
def foo(a: Int): Option[Int] = Some(a)
}
val fooResult: Foo[Result] = new Foo[Result] {
def foo(a: Int): Result[Int] = "foo".asLeft[Int]
}
import transformations._
val fooList = fooOpt.transform[List](toList)
println(fooList.foo(1))
val adapted = transformations.adaptError[Result, String] {
case "foo" => "bar"
case "bar" => "baz"
}
println("foo".asLeft[Int].transform(adapted))
val fooAdapted = fooResult.transform(adapted)
println(fooAdapted.foo(1))
type Target[A] = OptionT[Result, A]
val fooOptionT = fooOpt.transform(toOptionT[Result])
println(fooOptionT.foo(1))
trait Bar[F[_, _]]{
def bar(a: Int) : F[String, Int]
}
object Bar {
implicit val barFunctorK2: FunctorK2[Bar] = new FunctorK2[Bar] {
def mapK2[F[_, _], G[_, _]](alg: Bar[F], fk: FunctionK2[F, G]): Bar[G] =
new Bar[G] {
def bar(a: Int): G[String, Int] = fk(alg.bar(a))
}
}
}
val barEither : Bar[Either] = new Bar[Either]{
def bar(a: Int): Either[String, Int] = Left("foo")
}
val barSwapped = barEither.transform(transformations.swap)
println(barSwapped.bar(1))
val barOption : MonadicProjection[Bar, Option] = new MonadicProjection[Bar, Option]{
def bar(a: Int): Option[Int] = Some(a)
}
val barList = barOption.transform(toList)
println(barList.bar(1))
}
@Baccata
Copy link
Author

Baccata commented Oct 24, 2022

** Apologies for the mess, I haven't done the dishes yet **

So what the F[_] is going on here ? This is basically a sketch of an idea to leverage the AnyKind type bound from Scala 3 to offer a unified user experience when applying transformations on "stuff" where "stuff" can take the shape of values, functors, functor-algebras, bi-functor-algebras, etc.

The problem this solves is basically "can we have a single transform extension method that takes care of lifting things to the right level for us, so that we don't have to play type-puzzles in userland ?"

Can we go further ? Possibly, I'm not quite certain. The next step would be to formulate the Functor construct in a truly poly-kinded way (and Function/FunctionK). It feels like it should be possible, because the type-erased form of these functions is very much the same, but it may involve even more black magic than what appears in this snippet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment