Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Step by Step / Deep explain: The Power of (Co)Yoneda (preferably in scala) through Coroutines

some background code

/** FunctorStr: ∑ F[-]. (∏ A B. (A -> B) -> F[A] -> F[B]) */ trait FunctorStr[F[_]] { self =>   def map[A, B](f: A => B): F[A] => F[B] }  trait Yoneda[F[_], A] { yo =>    def apply[B](f: A => B): F[B]    def run: F[A] =     yo(x => x)    def map[B](f: A => B): Yoneda[F, B] = new Yoneda[F, B] {     def apply[X](g: B => X) = yo(f andThen g)  } }  object Yoneda {    implicit def yonedafunctor[F[_]]: FunctorStr[({ type l[x] = Yoneda[F, x] })#l] =     new FunctorStr[({ type l[x] = Yoneda[F, x] })#l] {       def map[A, B](f: A => B): Yoneda[F, A] => Yoneda[F, B] =         _ map f     }    def apply[F[_]: FunctorStr, X](x: F[X]): Yoneda[F, X] = new Yoneda[F, X] {     def apply[Y](f: X => Y) = Functor[F].map(f) apply x   } }     trait Coyoneda[F[_], A] { co =>    type I    def fi: F[I]    def k: I => A    final def map[B](f: A => B): Coyoneda.Aux[F, B, I] =     Coyoneda(fi)(f compose k)  }  object Coyoneda {    type Aux[F[_], A, B] = Coyoneda[F, A] { type I = B }    def apply[F[_], B, A](x: F[B])(f: B => A): Aux[F, A, B] =     new Coyoneda[F, A] {      type I = B      val fi = x      val k = f    }    implicit def coyonedaFunctor[F[_]]: FunctorStr[({ type l[x] = Coyoneda[F, x] })#l] =    new CoyonedaFunctor[F] {}    trait CoyonedaFunctor[F[_]] extends FunctorStr[({type l[x] = Coyoneda[F, x]})#l] {    override def map[A, B](f: A => B): Coyoneda[F, A] => Coyoneda[F, B] =      x => apply(x.fi)(f compose x.k)  }    def liftCoyoneda[T[_], A](x: T[A]): Coyoneda[T, A] =    apply(x)(a => a)   } 

Now I thought I understood yoneda and coyoneda a bit just from the types – i.e. that they quantify / abstract over map fixed in some type constructor F and some type a, to any type B returning F[B] or (Co)Yoneda[F, B]. Thus providing map fusion for free (? is this kind of like a cut rule for map ?). But I see that Coyoneda is a functor for any type constructor F regardless of F being a Functor, and that I don't fully grasp. Now I'm in a situation where I'm trying to define a Coroutine type, (I'm looking at https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/coroutines-for-streaming/part-2-coroutines for the types to get started with)

case class Coroutine[S[_], M[_], R](resume: M[CoroutineState[S, M, R]])  sealed trait CoroutineState[S[_], M[_], R]    object CoroutineState {     case class Run[S[_], M[_], R](x: S[Coroutine[S, M, R]]) extends CoroutineState[S, M, R]     case class Done[R](x: R) extends CoroutineState[Nothing, Nothing, R]     class CoroutineStateFunctor[S[_], M[_]](F: FunctorStr[S]) extends        FunctorStr[({ type l[x] = CoroutineState[S, M, x]})#l] {         override def map[A, B](f : A => B) : CoroutineState[S, M, A] => CoroutineState[S, M, B]         =         { ??? }     }   } 

and I think that if I understood Coyoneda better I could leverage it to make S & M type constructors functors way easy, plus I see Coyoneda potentially playing a role in defining recursion schemes as the functor requirement is pervasive.

So how could I use coyoneda to make type constructors functors like for example coroutine state? or something like a Pause functor ?

like image 832
Mzk Levi Avatar asked Jun 02 '14 17:06

Mzk Levi


1 Answers

The secret of Yoneda is that it "defers" the need for the Functor instance a bit. It's tricky at first because we can define instance Functor (Yoenda f) without using f's Functor instance.

newtype Yoneda f a = Yoneda { runYoneda :: forall b . (a -> b) -> f b }  instance Functor (Yoneda f) where   fmap f y = Yoneda (\ab -> runYoneda y (ab . f)) 

But the clever part about Yoneda f a is that it's supposed to be isomorphic to f a, however the witnesses to this isomorphism demand that f is a Functor:

toYoneda :: Functor f => f a -> Yoneda f a toYoneda fa = Yoneda (\f -> fmap f fa)  fromYoneda :: Yoneda f a -> f a fromYoneda y = runYoneda y id 

So instead of appealing to the Functor instance for f during definition of the Functor instance for Yoneda, it gets "defered" to the construction of the Yoneda itself. Computationally, it also has the nice property of turning all fmaps into compositions with the "continuation" function (a -> b).

The opposite occurs in CoYoneda. For instance, CoYoneda f is still a Functor whether or not f is

data CoYoneda f a = forall b . CoYoneda (b -> a) (f b)  instance Functor (CoYoneda f) where   fmap f (CoYoneda mp fb) = CoYoneda (f . mp) fb 

however now when we construct our isomorphism witnesses the Functor instance is demanded on the other side, when lowering CoYoenda f a to f a:

toCoYoneda :: f a -> CoYoneda f a toCoYoneda fa = CoYoneda id fa  fromCoYoneda :: Functor f => CoYoneda f a -> f a fromCoYoneda (CoYoneda mp fb) = fmap mp fb 

Also we again notice the property that fmap is nothing more than composition along the eventual continuation.

So both of these are a way of "ignoring" a Functor requirement for a little while, especially while performing fmaps.


Now let's talk about this Coroutine which I think has a Haskell type like

data Coroutine s m r = Coroutine { resume :: m (St s m r) } data St s m r = Run (s (Coroutine s m r)) | Done r  instance (Functor s, Functor m) => Functor (Coroutine s m) where   fmap f = Coroutine . fmap (fmap f) . resume  instance (Functor s, Functor m) => Functor (St s m) where   fmap f (Done r) = Done (f r)   fmap f (Run s ) = Run (fmap (fmap f) s) 

This instance this requires Functor instances for both the s and m types. Can we do away with them by using Yoneda or CoYoneda? Basically automatically:

data Coroutine s m r = Coroutine { resume :: CoYoneda m (St s m r) } data St s m r = Run (CoYoneda s (Coroutine s m r)) | Done r  instance Functor (Coroutine s m) where   fmap f = Coroutine . fmap (fmap f) . resume  instance Functor (St s m) where   fmap f (Done r) = Done (f r)   fmap f (Run s ) = Run (fmap (fmap f) s) 

but now, given that I used CoYoneda, you'll need Functor instances for both s and m in order to extract s and m types out of your Coroutine. So what's the point?

mapCoYoneda :: (forall a . f a -> g a) -> CoYoneda f a -> CoYoneda g a mapCoYoneda phi (CoYoneda mp fb) = CoYoneda mp (phi fb) 

Well, if we have a natural transformation from our f to a g which does instantiate Functor then we can apply that at the end in order to extract our results. This structural mapping will only apply once and then, upon evaluating fromCoYoneda, the entire stack of composed fmapped functions will hit the result.


Another reason why you might want to play with Yoneda is that it's sometimes possible to get Monad instances for Yoneda f even when f isn't even a Functor. For instance

newtype Endo a = Endo { appEndo :: a -> a }  -- YEndo ~ Yoneda Endo data YEndo a = YEndo { yEndo :: (a -> b) -> (b -> b) }  instance Functor YEndo where   fmap f y = YEndo (\ab -> yEndo y (ab . f))  instance Monad YEndo where   return a = YEndo (\ab _ -> ab a)   y >>= f  = YEndo (\ab b -> yEndo y (\a -> yEndo (f a) ab b) b) 

where we get the definition of Monad YEndo by thinking of YEndo as a CPS transformed Maybe monad.

This kind of work obviously isn't useful if s must be left general, but can be beneficial if instantiating Coroutine concretely. This example was taken directly from Edward Kmett's post Free Monads for Less 2.

like image 103
J. Abrahamson Avatar answered Oct 03 '22 09:10

J. Abrahamson