spartanz / schemaz

A purely-functional library for defining type-safe schemas for algebraic data types, providing free generators, SQL queries, JSON codecs, binary codecs, and migration from this schema definition
https://spartanz.github.io/schemaz
Apache License 2.0
164 stars 18 forks source link

Avoid going through the "tuple representation" #44

Open vil1 opened 5 years ago

vil1 commented 5 years ago

This should be done after #38.

We should search for to avoid going through the "tuple representation" of records in the functors we derive.

Currently, when processing a record like:

final case class Foo(i: Int, s: String, b: Boolean)

The functors we derive from schemas first convert instances of Foo to a value of type (Int, (String, Boolean)); there should be a way of expressing derivations that avoids this unnecessary step.

Solving this issue might require to change the type of the iso field in Union and Record, currently an Iso[A0, A].

vil1 commented 5 years ago

I have a bunch of vague ideas about this issue, I will try to see how far I can get.

vil1 commented 5 years ago

Writing down my main idea with the hope someone will be able to help me realising it.

It is about avoiding going through the tuple representation of records and only for interpreting into a covariant target (you have to start somewhere ^^).

lets take an example case class:

final case class Foo(i: Int, s: String, b: Boolean)

and a corresponding schema (isomorphisms eluded for brevity)

val foo = record( 
  "i" -*>: prim(ScalaInt) :*:
  "s" -*>: prim(ScalaString) :*:
  "b" -*>: prim(ScalaBool)
)

Which is a tree with this shape :

       Record
         |
        :*:
       /   \
    Int    :*:
          /   \
      String  Boolean

Since Foo is a case class, we have access to a curried function of type Int => String => Boolean => Foo:

val curriedCtr: Int => String => Boolean => Foo = (Foo.apply _).curried

Now, imagine we're able to carry some "context" along the traversal of our tree and that this context contains "some F[_]" where F is our target functor (and there is an Applicative[F] instance available).

When reaching the Record node while going down (the very first step of the recursion), we put F.point(curriedCtr) in our context (of type F[Int => String => Boolean => Foo].

After a few step we reach a first leaf (the Int node), and the algebra returns an F[Int] so we can combine it with the F[Int => String => Boolean => Foo] in our context (using ap) to obtain an F[String => Boolean => Foo] that we put back in our context.

We repeat the same mechanism when we reach the String leaf, using the produced F[String] to update the context to an F[Boolean => Foo].

Finally, when we reach the last leaf and repeat the same operation again, we end up with an F[Foo] in our context.

Traversing back up the two :*: nodes is a no-op, and getting back up to the Record node, we just return the F[Foo] we have in the context. The important point being that this F[Foo] never builds nor destructs any tuple to perform its work.

If you've read thus far, you probably agree that this sounds promising.

With an hyloM (to be implemented) using State to maintain the so-called context, we should be able to implement the traversal described above (I can detail that claim further if needed).

But the problem is that the type of the said context would have to change during the traversal (from F[Int => String => Boolean => Foo] to F[Foo] and everything in between).

Moreover, we must make sure that every time we reach a leaf Schema[A], the context is of type F[A => X] (for some type X), so a naive approach that would build this context around the existential type F[_] isn't an option.

So far, I have no idea how to achieve that...