typelevel / cats-tagless

Library of utilities for tagless final encoded algebras
https://typelevel.org/cats-tagless/
Apache License 2.0
314 stars 41 forks source link

Invocation data type and ReifiedInvocations type class #15

Open notxcain opened 5 years ago

notxcain commented 5 years ago

I want to drop a support of Liberator in favour of cats-tagless. But there are two very useful types that I need. And I think they may be a good fit for cats-tagless.

  1. Invocation[M[_[_]], A]

    trait Invocation[M[_[_]], A] {
    def invoke[F[_]](target: M[F]): F[A]
    }

    allows to encode some call of M[F] resulting in F[A] it's like a dual of Free algebra base functor.

  2. ReifiedInvocations[M[_[_]]] type class

    trait ReifiedInvocations[M[_[_]]] extends FunctorK[M] { // This inheritance is arguable
    def invocations: M[Invocation[M, ?]]
    def mapInvocations[F[_]](f: Invocation[M, ?] ~> F): M[F] = mapK(invocations, f)
    }

    allows to create an instance of M[Invocation[M, ?]] that is very useful for transformation of tagless algebras (see Aecor). There is also a macro for its derivation.

If you find this interesting I will send a PR.

P.S. One interesting intuition is that M[F] is a collection of applied F[_] indexed by Invocation[M, A] :)

kailuowang commented 5 years ago

Definitely interesting to me. I also remember seeing mentioning of interest in this. A PR would be great!

LukaJCB commented 5 years ago

I'm not sure I fully understand ReifiedInvocation but Invocation can be seen as a specialization of Program :) see here: https://github.com/typelevel/cats-tagless/blob/master/core/src/main/scala/cats/tagless/optimize/Program.scala

notxcain commented 5 years ago

@LukaJCB Program is fine as a replacement for Invocation if there is a constraint that exists for all F[_], e.g. some Unconstrained[F[_]] Another example for ReifiedInvocations is this:

def router[K, F[_]: Concurrent, M[_[_]]: ReifiedInvocations: FunctorK](load: K => F[M[F]): F[K => M[F]] =
  MVar[F].of(Map.empty[K, M[F]]).map { instancesVar =>
    { key: K =>
      ReifiedInvocations[M].invocations.mapK {
        new (Invocation[M, ?] ~> F) {
          override def apply[A](invocation: Invocation[M, A]): F[A] =
            for {
              instances <- instancesVar.take
              mf <- instances.get(key) match {
                     case Some(mf) => instancesVar.put(instances) >> mf.pure[F]
                     case None =>
                       load(key).flatMap { mf =>
                         instancesVar.put(instances.updated(key, mf)).as(mf)
                       }
                   }
              a <- invocation.invoke(mf)
            } yield a
        }
      }
    }
  }

So it allows you to transform some M[_[_]] instance, even if you don't have it yet. In other words ReifiedInvocations#invocations makes it possible to create an instance that does nothing other than captures method invocations, which you can run later against some other instance.