Closed fommil closed 6 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
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
)
The implementations are consistent with the Haskell versions. I doubt it's a bug.
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)))
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.
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.
@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.
I should say the type is "simplified" not as in losing information, just rewritten to remove the once-inhabited function.
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
.
conquer
is not implicit
so it cannot really produce type class by it-self, so no orphans.
@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]
.
@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)))
@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.
@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.
Thanks all, that's pretty useful.
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?
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.
@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.
@jbgi yeah, I think an invariant Apply/Divide is missing!
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
.
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.
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)
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.
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/
I have a lot of reading / understanding to do to be able to respond to that question.
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.
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
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.
@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.
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
?
@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?
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 Either
s 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)
}
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
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).
My prototype now works
A few things to do before I will start working on the macros
acceptX
methods in terms of Plus
or Plus
in terms of thisunit
/ counit
)btw having by-name parameters can make it super efficient.
@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.
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)))
}
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.
@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
).
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:
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).
@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
.
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?
@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
}
@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)))
}
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.
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.
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.
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.)
ok, I have two ideas on how to proceed. Will try to spike it in the next few days....
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:
how can this ever work? In scala we'd typically need to take evidence for
Divisible[A]
in theconquer
parameter, the current signature means that we must ignore theA
.In
scalaz-deriving
I'm probably going to base contravariant derivations onDivide
(or some variant with labels)// @xuwei-k (you touched it last)