scalaz / scalaz

Principled Functional Programming in Scala
Other
4.67k stars 704 forks source link

[scalaz-deriving] Invariant parent of Divide/Apply and Decidable/Alternative #1481

Closed fommil closed 6 years ago

fommil commented 7 years ago

This was originally opened as a question about Divisible.conquer but morphed into the realisation that scalaz-deriving will need an invariant parent of Apply and Divide to support typeclass derivation.

I'll pick this up... I'll create a fresh hierarchy in the stalactite repo and ask for comments there but I'd very much like to get a bincompat version of this into 7.2

original ticket follows:


trait Divisible[F[_]] extends Divide[F] {
  def conquer[A]: F[A]
}

how can this ever work? In scala we'd typically need to take evidence for Divisible[A] in the conquer parameter, the current signature means that we must ignore the A.

In scalaz-deriving I'm probably going to base contravariant derivations on Divide (or some variant with labels)

// @xuwei-k (you touched it last)

xuwei-k commented 7 years ago

see Divisible[Equal] and Divisible[Order] instances.

My scalaz implementation just ported from ekmett/contravariant library. https://github.com/ekmett/contravariant/blob/v1.4/src/Data/Functor/Contravariant/Divisible.hs#L118 /cc @ekmett

fommil commented 7 years ago

these implementations look wrong... doesn't this say that two A are always === ? Shouldn't it return the actual Equal[A]? (which I have access to in my closure, btw, so I don't need to call conquer)

puffnfresh commented 7 years ago

The implementations are consistent with the Haskell versions. I doubt it's a bug.

puffnfresh commented 7 years ago

We can derive contravariant liftA like so:

def liftD[F[_]: Divisible, A, B](fa: F[A], f: B => A): F[B] =
  Divisible[F].divide(Divisible[F].conquer[Unit], fa)(c => ((), f(c)))
fommil commented 7 years ago

from my understanding of divide and conquer, you can divide up a problem in to sub parts (providing the mechanism for doing the splitting) e.g.

scala> case class Foo(s: String, i: Int)
scala> implicit val fooEqual: Equal[Foo] =
         Divide[Equal].divide2(Equal[String], Equal[Int]) {
           (foo: Foo) => (foo.s, foo.i)
         }
scala> Foo("foo", 1) === Foo("bar", 1)
res: Boolean = false

(made simpler with deriveX) but this Equal[String] or Equal[Int] is not going to match Divisible[Equal].conquer[String] or Int since it's the trivial "always true" case.

fommil commented 7 years ago

shouldn't conquer then only return the F[Unit]? I can see why it makes sense for Equal[Unit] here. Otherwise we are intentionally introducing orphans.

puffnfresh commented 7 years ago

@fommil we don't want f () because that's a much weaker claim. This code is derived from the idea of a contravariant applicative where the original type looks like:

conquer :: (a -> ()) -> f a

But this is simplified. I think the above type is what you're getting at, though.

puffnfresh commented 7 years ago

I should say the type is "simplified" not as in losing information, just rewritten to remove the once-inhabited function.

fommil commented 7 years ago

right, so I think the claim we're making is too strong... conquer[A]: F[A] implies we can return a legitimate F[A] but it seems that even in trivial examples like Equal we end up just creating dud orphans like Equal[String] that always return true.

jbgi commented 7 years ago

conquer is not implicit so it cannot really produce type class by it-self, so no orphans.

puffnfresh commented 7 years ago

@fommil having these type-classes defined for other type-classes is the broken part. We should not have Divisible[Equal]. We should have Divisible[Equivalence].

fommil commented 7 years ago

@jbgi good point, it's not implicit. I think I'll just avoid Divisible.

@puffnfresh hmm, I'm not sure about that. My plan for scalaz-deriving is to have a Divide[F] and/or Apply[F] to enable product derivations for an F (instead of the exceptionally complex shapeless approach). What do you mean by Equivalence here and why do you say it's broken?

Hmm, isn't your liftD just contramap?

  override def contramap[A, B](fa: F[A])(f: B => A): F[B] =
    divide(conquer[Unit], fa)(c => ((), f(c)))
jdegoes commented 7 years ago

@fommil For the base case, you can use conquer[Equal[Unit]] like @puffnfresh's example (e.g. every product is Unit /\ A /\ B /\ ... /\ Z). Or if you just use the divideX you won't even need conquer.

In any case, for type class derivation, conquer applied to anything except TC[Unit] (where TC[_] is the type class) would probably be a bug.

conquer is really (A => ()) => F[A], the contravariant version of point ((() => A) => F[A]).

One can imagine a stop between Apply and Applicative that provides a weaker pointUnit ((() => ()) => F[Unit]), in the same way one could imagine a stop between Divide and Divisible (conquerUnit : (() => ()) => F[Unit], i.e. F[Unit]), but I'm not sure such a thing would necessarily have the same kind of laws that you want it to have.

What you really want is some kind of law that guarantees, roughly, that the type class for A and (A, conquerUnit) have the same behavior, modulo irrelevant details. Worth thinking on for a bit.

puffnfresh commented 7 years ago

@fommil yes, liftA is a derived fmap, liftD is a derived contramap. Just showing that it's possible to derive Contravariant from conquer and divide.

fommil commented 7 years ago

Thanks all, that's pretty useful.

jbgi commented 7 years ago

on a related note, yesterday at work we were wondering if there was a way to derive, say, Semigroup in a similar fashion that we derive Order via Divide. eg. I wanted to derive a Semigroup for a datatype that is isomorphic to (NonEmptyList[A], NonEmptyList[B]). I'm guessing that what is missing is a kind of InvariantFunctor => InvariantDivide type class... ? But maybe it is better to leave this use-case to libs like stalactite or magnolia...?

any thought?

fommil commented 7 years ago

that's exactly what stalactite will do :smile:

I expect I'll have to introduce something that is similar to Divide, but with more bells and whistles, and probably some intermediate wrapper around the typeclass.

jdegoes commented 7 years ago

@jbgi With 8, I think we need to push invariant all the way down. It's inconsistent in 7. Libraries like scodec have shown there is a use for the invariant abstractions.

EDIT: We could also do that with 7 if there's interest.

puffnfresh commented 7 years ago

@jbgi yeah, I think an invariant Apply/Divide is missing!

fommil commented 7 years ago

yeah, I wonder if that's exactly what scalaz-deriving needs.... Ed Kmett said something about Profunctor at Scala World but I read the API and didn't really understand how that would help.

Is there somewhere I can read further on this? (and I assume you mean something that combines both Apply and Divide functionality in a single trait at the invariant level). A very solid and realistic example of the need for this is to support typeclass derivation of a typeclass that has the type parameters in both covariant and invariant position, e.g. a Format that extends from an Encoder and a Decoder.

fommil commented 7 years ago

the other thing (besides labelling the products) I need to experiment with is if the API should have byname or eager parameters... jmh will reveal all. I have some plans for caching the implicits.

I will have more to share next week! I'm planning on spending most of next week's evenings on this.

puffnfresh commented 7 years ago

An invariant Apply/Divide functor would look like:

import Data.Functor.Invariant

class Invariant f => ApplyDivide f where
  apdivide :: (a -> b -> c) -> (c -> (a, b)) -> f a -> f b -> f c

-- Example

data Semigroup' a
  = Semigroup' { append :: a -> a -> a }

instance Invariant Semigroup' where
  invmap f g (Semigroup' h) =
    Semigroup' $ \a b -> f $ g a `h` g b

instance ApplyDivide Semigroup' where
  apdivide f g (Semigroup' h) (Semigroup' i) =
    Semigroup' $ \a b ->
                   let (gaa, gab) = g a
                       (gba, gbb) = g b
                   in f (h gaa gba) (i gab gbb)

firstSemigroup :: Semigroup' a
firstSemigroup =
  Semigroup' const

lastSemigroup :: Semigroup' a
lastSemigroup =
  Semigroup' $ flip const

firstAndLastSemigroup :: Semigroup' (a, b)
firstAndLastSemigroup =
  apdivide (,) id firstSemigroup lastSemigroup

And we get:

append firstAndLastSemigroup (1, 2) (3, 4) == (1,4)
fommil commented 7 years ago

yup, sweet, apdivide is exactly what we need. The combination of applying and divide & conquer sounds exactly like the sort of thing that Khan would do. That shall be the working title.

ekmett commented 7 years ago

Excerpted from another email thread:

There are at least 4 notions of 'xmap' that are worth talking about. This is one reason why I haven't packaged it up personally.

For xmap f g do you require f . g = id ? g . f = id? both? neither?

Each of these gives rise to a different notion of 'C' in the definition of Day above. Its asking for just the split monomorphisms, split epimorphisms, isomorphisms or just function pairs.

We can talk about Day convolution for each of these settings for sums or products as the choice of tensor on C.

This is what I was alluding to during our talk at Scala World.

Not to using the existing classes.

given the explosion of related classes, an alternative would be to define

class Productive f where product :: f a -> f b -> f (a, b) unit :: f ()

with the strongest of conditions we want bolted into f, namely we need associativity etc.

so we'd need at least:

assoc :: Iso' (f ((a, b), c)) (f ((a, b), c)) swap :: Iso' (f (a,b)) (f (b,a)) lambda :: Iso' (f ((),a)) (f a) rho :: Iso' (f (a, ()) (f a)

where the usual monoidal category laws hold

for the first class. (Or to accept something more general that lets you lift real isomorphisms)

and then build up Divisible by using such a Contravariant f + Productive f and Applicative by using Functor f + Productive f.

and we can build

class Coproductive f where coproduct :: f a -> f b -> f (Either a b) counit :: f Void

with similar associativity and unit conditions or xmap-restricted-to-real-isos.

Then we can almost get Decidable out of this + Contravariant and Alternative out of this plus Functor.

I say almost because the laws specifying Alternative are underspecified and we didn't talk about what extra distributive laws we really want.

So, which of those notions of Invariant is your Invariant, anyways? e.g. I used a section-retract restricted version in in http://comonad.com/reader/2008/rotten-bananas/

fommil commented 7 years ago

I have a lot of reading / understanding to do to be able to respond to that question.

fommil commented 7 years ago

I updated the ticket and assigned it to myself. I don't want to create a new ticket because it'll just end up referring back here so much.

puffnfresh commented 7 years ago

Alright, I think I understand the relationship with the Day convolution's variance and these classes.

Covariant Day:

data Day f g a = forall b c. Day (f b) (g c) (b -> c -> a)
class Functor f => Apply f where
  ap :: Day f f a -> f a

Contravariant Day:

data Day f g a = forall b c. Day (f b) (g c) (a -> (b, c))
class Contravariant f => Divide f where
  divide :: Day f f a -> f a

Invariant Day:

data Day f g a = forall b c. Day (f b) (g c) (b -> c -> a) (a -> (b, c))
class Invariant f => Invent f where
  invent :: Day f f a -> f a

Phil Freeman showed a way of extending Profunctor:

http://try.purescript.org/?backend=core&gist=d0ad7554c7592e2c12a271e8aafefa9d

fommil commented 7 years ago

right, this Profunctor thing is exactly what I didn't get.

For scalaz7, I think having a common invariant parent is the right way to go.

jdegoes commented 7 years ago

@ekmett Building up Applicative and Divisible from Productive and Coproductive is a really interesting factoring of the hierarchy.

For xmap f g do you require f . g = id ? g . f = id? both? neither?

Indeed, I have something like:

data Equiv a b = Equiv { 
  leftCanon :: a -> a,
  rightCanon :: b -> b,
  to :: a -> b,
  from :: b -> a }

where from . to . leftCanon = leftCanon, to . from . rightCanon = rightCanon.

This is a very useful relation (it's an isomorphism between sets partitioned by equivalence relations, defined by choosing a canonical element for each equivalence class). It permits all the same structures you would expect (Apply, Functor, etc.). Yet neither introducing parallel hierarchies or trying (and probably failing) to model this categorically in Scala seem very attractive.

ekmett commented 7 years ago

If you have either f . g = id or g . f = id then both f . g . f = f, and g . f . g = g. Why do you need leftCanon and rightCanon in Equiv?

jdegoes commented 7 years ago

@ekmett You're right, I don't need those, since leftCanon = f . g . f, and rightCanon = g . f . g.

Do you have any thoughts on how these use cases could / should affect the standard class hierarchy?

fommil commented 7 years ago

for contravariant coproducts, this is how I'm translating the above in a simple example, the generalisation to chooseX should be reasonably obvious. I'm not keen on nesting Eithers so a more efficient data structure may be in order... plus I'm not keen on the enforced X limit (same problem with divide/apply/alternative)

// Copyright: 2017 https://gitlab.com/fommil/stalactite/graphs
// License: http://www.gnu.org/licenses/lgpl-3.0.en.html
package stalactite

import java.lang.String
import scala.inline
import scala.{ Boolean, Int }
import scalaz._, Scalaz._
import scala.Predef.{ ???, identity }

// https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html#t:Decidable
trait Decidable[F[_]] {
  def lose[A]: F[A]
  final def choose[A, B, C](fb: F[B], fc: F[C])(f: A => B \/ C): F[A] =
    choose2(fb, fc)(f)

  def choose2[A, B, C](fb: F[B], fc: F[C])(f: A => B \/ C): F[A]
  def choose3[A, B, C, D](fb: F[B], fc: F[C], fd: F[D])(
    f: A => B \/ (C \/ D)
  ): F[A] = {
    val fcd: F[C \/ D] = choose2(fc, fd)(identity)
    choose2(fb, fcd)(f)
  }

  // ... chooseX
}

// covariant should use
// MonadPlus
// https://hackage.haskell.org/package/base-4.8.2.0/docs/Control-Applicative.html#t:Alternative

object Decidable {
  @inline def apply[F[_]](implicit i: Decidable[F]): Decidable[F] = i

  implicit val equal: Decidable[Equal] = new Decidable[Equal] {
    def lose[A]: Equal[A] = ???
    def choose2[A, B, C](fb: Equal[B], fc: Equal[C])(
      f: A => B \/ C
    ): Equal[A] = new Equal[A] {
      def equal(a1: A, a2: A): Boolean =
        (f(a1), f(a2)) match {
          case (-\/(b1), -\/(b2)) => fb.equal(b1, b2)
          case (\/-(c1), \/-(c2)) => fc.equal(c1, c2)
          case _                  => false
        }
    }
  }
}

object EqualExample extends scala.App {

  sealed trait Foo
  final case class Bar(s: String)  extends Foo
  final case class Baz(i: Int)     extends Foo
  final case class Faz(b: Boolean) extends Foo

  implicit val barE: Equal[Bar] = Equal[String].contramap(_.s)
  implicit val bazE: Equal[Baz] = Equal[Int].contramap(_.i)
  implicit val fazE: Equal[Faz] = Equal[Boolean].contramap(_.b)

  implicit val foo: Equal[Foo] =
    Decidable[Equal].choose3(barE, bazE, fazE) {
      case bar: Bar => -\/(bar)
      case baz: Baz => \/-(-\/(baz))
      case faz: Faz => \/-(\/-(faz))
    }

  val bar: Foo = Bar("hello")
  val baz: Foo = Baz(1)
  val faz: Foo = Faz(true)

  import scala.Predef.println
  println(bar === bar)
  println(bar === baz)
  println(baz === bar)
  println(baz === baz)

  println(bar === faz)
  println(baz === faz)
  println(faz === faz)
}
fommil commented 7 years ago

I'm still not getting how Alternative helps .... I'm creating a typeclass that looks like this. Anyone recognise it from Haskell?

trait Undecidable[F[_]] {
  def win[A]: F[A]
  def accept[A, B, C](fb: F[B], fc: F[C])(f: B \/ C => A): F[A] = ...
}

i.e. (Either b c -> a) -> f b -> f c -> f a I tried hoogle

jdegoes commented 7 years ago

With Alternative, you can do:

val fa: F[A] = ???
val fb: F[B] = ???
val fab : F[A \/ B] = fa.map[A \/ B](-\/(_)) <|> fb.map[A \/ B](\/-(_))

Then you can map over F[A \/ B] with f: A \/ B => C to get F[C] (thus, at least, matching the types, if not the laws, of accept). The empty of Alternative may be your win (I am not sure).

fommil commented 7 years ago

My prototype now works

A few things to do before I will start working on the macros

btw having by-name parameters can make it super efficient.

fommil commented 7 years ago

@jdegoes the problem with Alternative is that I don't think all typeclasses have a universally quantified method. e.g. what should empty be? I'm certainly not thinking of using it, but I'm guessing it can be used to derive ap or something crazy like that

trait Default[A] { def default: A }
object Default {
  implicit val undecidable: Undecidable[Default] = new Undecidable[Default] {
    def ap[A, B](fa: => Default[A])(f: => Default[A => B]): Default[B] = pure(f.default(fa.default))
    def point[A](a: => A): Default[A] = new Default[A] { override def default: A = a }
    def empty[A]: Default[A]                                   = ??? // err...
    def plus[A](fa: Default[A], fb: => Default[A]): Default[A] = fa
  }

  implicit val int: Default[Int]         = 0.pure[Default]
  implicit val string: Default[String]   = "".pure[Default]
  implicit val boolean: Default[Boolean] = false.pure[Default]
}

also, I really wish it was possible to tell scala to make a concrete method abstract again. It makes a lot more sense to implement map than ap, accept2 than plus, and apply2 sort of feels natural.

fommil commented 7 years ago

ok, after rearranging things a little (what I'm calling Codecidable could be a parent of Alternative), this is what a typical typeclass deriver should look like for covariant typeclasses

  implicit val codecidable: Codecidable[Default] = new Codecidable[Default] {
    def map[A, B](fa: Default[A])(f: A => B): Default[B] =
      instance(f(fa.default))

    def product2[A, B, C](fb: => Default[B], fc: => Default[C])(
      f: (B, C) => A
    ): Default[A] = instance(f(fb.default, fc.default))

    def coproduct2[A, B, C](fb: => Default[B], fc: => Default[C])(
      f: B \/ C => A
    ): Default[A] = instance(f(-\/(fb.default)))
  }
fommil commented 7 years ago

recursive ADTs and GADTs cause stack overflows... I was half expecting this. The easy hack is to use shapeless Lazy and Cached everywhere. But that's sad. Maybe some recent compiler improvements will fix it.

jdegoes commented 7 years ago

@fommil That looks quite a bit simpler. I notice it's exactly Functor + @ekmett's Productive + Coproductive, minus the trivial def unit: F[Unit] and def counit: F[Void] (you derive product2 and coproduct2 from product and coproduct plus Functor's map).

fommil commented 7 years ago

Nice. BTW, for performance I think I need to have my own Divide (which can go into scalaz 7.3) with by-name parameters.

Also, I got it working with ADTs having recursive types :smile:

fommil commented 7 years ago

I might cut a release in a few days without the macro... so I can play around with the boilerplate version on other projects and see if I'm missing anything (also it'll take me some time to write the macro and I really don't want to iterate on what it produces).

jdegoes commented 7 years ago

@fommil Sounds like a good idea to me. Also agreed on specializing with by-name parameters, which we can wrap into 7.3.

The Productive / Coproductive factoring of the problem also looks sufficiently interesting for possible inclusion into 8.0.

Looks like Scalaz has PlusEmpty, by the way, so you can get the interesting part out of Alternative (/ApplicativePlus) without the unnecessary and non-sensical point.

fommil commented 7 years ago

empty is tricky... typeclasses don't necessarilly have a universally quantified instance, so it doesn't work. Plus could extend from Coproductive

unit and counit might work though... but what to use as Void in scala?

jdegoes commented 7 years ago

@fommil Yeah, you're right, maybe just Plus without empty could work?

I am amazed we don't have Void. We need to add it:

final abstract class Void private () {
  def absurd[A]: A
}
fommil commented 7 years ago

@puffnfresh I added the derived contramap to my mirror of the typeclass hierarchy and it works beautifully! This is a typical example for a contravariant typeclass (Equal)

  implicit val tcdEqual: TypeclassDerivation[Equal] = new Codivide[Equal] {
    def conquer[A] = Equal.equal((_, _) => true)
    def divide2[A, B, C](fa: => Equal[A], fb: => Equal[B])(f: C => (A, B)) =
      Equal.equal[C] { (c1, c2) =>
        val (a1, b1) = f(c1)
        val (a2, b2) = f(c2)
        fa.equal(a1, a2) && fb.equal(b1, b2)
      }

    def codivide2[A, B, C](fb: => Equal[B], fc: => Equal[C])(
      f: A => B \/ C
    ): Equal[A] = { (a1, a2) =>
      (f(a1), f(a2)) match {
        case (-\/(b1), -\/(b2)) => fb.equal(b1, b2)
        case (\/-(c1), \/-(c2)) => fc.equal(c1, c2)
        case _                  => false
      }
    }
}

and a covariant typeclass

  implicit val coproductive: TypeclassDerivation[Default] =
    new Coproductive[Default] {
      def point[A](a: => A): Default[A] = instance(a)

      def product2[A, B, C](fb: => Default[B], fc: => Default[C])(
        f: (B, C) => A
      ): Default[A] = instance(f(fb.default, fc.default))

      def coproduct2[A, B, C](fb: => Default[B], fc: => Default[C])(
        f: B \/ C => A
      ): Default[A] = instance(f(-\/(fb.default)))
    }
fommil commented 7 years ago

I'm getting really stuck on how to support labels.

Here's a simple demonstration of the problem...

Say I want to make the Show for a product look like

(_1="foo",_2="bar")

The brute force approach would be to duplicate the entire Divide hierarchy and instead take a function

f: C => ((String, A), (String, B))

but it feels like there should be a clean way of doing this without having to go to such lengths. Plus, it's a little more complicated than that when we consider divide3+. Maybe it needs to be more like

f: C => ((String, A), B)

or, in addition to F => (A, B) we also have an F => String

But then we are saying there is something special about the left hand side, consider divide4 which may be merging the results of two divide2 calls, which doesn't need to have the labels at all.

  implicit val tcdShow: LazyDivisible[Show] = new LazyDivisible[Show] {
    def conquer[A]: Show[A] = Show.shows(_ => "")
    def divide2[A, B, C](fa: => Show[A], fb: => Show[B])(
      f: C => (A, B)
    ): Show[C] = Show.shows { c => ??? }

Thoughts appreciated. I really don't want to duplicate the entire hierarchy for typeclasses that need the labels.

fommil commented 7 years ago

is this as simple as having an extra typeclass provide the field name? I am reluctant to do that because it would effectively mean exploiting orphan instances that are scoped to the derivation for a specific product or coproduct. And it might cause problems for, e.g. a case class like

case class Foo(a: String, b: String, c: String, d: String)

if the divide4 splits it into two (String, String) (String, String) sides... we'd get ambiguous implicits there.

jdegoes commented 7 years ago

When you generate code to invoke the type class, why not just generate it for tuples in the form (String, A), or perhaps (Symbol, A), for case classes? In this case the type class would not need to change. Or you could generate a custom final case class Field[A](name: String, value: A) and require the user implement their type class for this special, library-defined type — in addition to implementing derivation foe their type class.

fommil commented 7 years ago

that's the Label approach I was thinking about .... but there are problems of going down that route. I've not given up on it yet but it seemed too fiddly.

My latest thinking that I'll try to spike, is to have a Labelled wrapper for the typeclass that adds support for field: Option[String], and make use of Tag to pass the field information along. e.g. for

case class Foo(a: String, b: String, c: String, d: String)

I wouldn't just be passing along Show[String] four times, I'd be passing along Show[String @@ "a"], Show[String @@ "b"], etc... (probably it'll look a lot uglier than that)

It needs to be Option to deal with the higher arity cases like divide4 where it is splitting the work into two divide2 calls and the leaves need the fields but the branches don't. The macro would generate all the necessary boilerplate on the companion. But I have a limit as to how far I want to push the type / implicit resolution system, because I know it is fragile.

I'm also interested in thinking about how dependent types or type lambdas could help to have a sort of heterogeneous list to take care of the arity. What I need is a data structure where I can say "I have a list of: a String, a value of some type and a typeclass for that type. But all the types are different for each entry."

Basically

  case class Content[F[_], A](label: String, value: A, typeclass: F[A])

then for a given typeclass, e.g. Show, I'd be dealing with

  List[Content[Show, ?]]

where all the types are the same, but I'd be able to --- given any "value" and "typeclass" --- be able to say

  .map { content =>
    content.label + "=" + content.typeclass.shows(content.value)
  }

Or maybe just think about the whole thing in terms of Foldable operations. Incidentally, this could replace the need for divideX and applyX in the current hierarchy. Or at least be an implementation for them.

Because he's using macro magic, Jon Pretty is able to do it this way... https://github.com/propensive/magnolia/blob/7b776425828d27b3112ad5bddafaa7564c326536/examples/src/main/scala/typeclasses.scala#L14-L27

(btw scalaz-deriving will support magnolia ... Jon has a few tickets to make it compatible. I think there is still lots of room for the more Category Theoretic approach of this thread. When magnolia breaks, nobody except Jon will be able to help.)

fommil commented 7 years ago

ok, I have two ideas on how to proceed. Will try to spike it in the next few days....