Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] #1234 - Leibniz & Liskov #1314

Closed
wants to merge 13 commits into from
Closed

[WIP] #1234 - Leibniz & Liskov #1314

wants to merge 13 commits into from

Conversation

hobwekiva
Copy link
Contributor

@hobwekiva hobwekiva commented Jan 16, 2017

I have ported some of the original Leibniz / Liskov functionality over to scalaz8 branch.

  • I am still not clear on whether the upper and lower bounds on Leibniz are needed. I understand that without them subst[F] on F[_ <: X] won't work, but is it really ever used in such a context?
  • I changed some of the names, but I am happy to stick with scalaz7 naming if that's an issue.
  • It is possible to generalize Leibniz to higher-kinded types (e.g. equality between type constructors).
  • There are no lower functions yet since there is no Injective typeclass.
  • A lot of Liskov companion object functions are still missing.
  • Should I keep all companion object functions in a ...Functions trait?
  • =:= and <:< might have most of the required functionality (most importantly substitute) in 2.13.
  • I have adapted some ideas from a similar PR to cats by @tel. The top comment in Leibniz is from the article.

@TomasMikula
Copy link
Member

I am still not clear on whether the upper and lower bounds on Leibniz are needed. I understand that without them subst[F] on F[_ <: X] won't work, but is it really ever used in such a context?

I have used it in such context before.

@hobwekiva
Copy link
Contributor Author

hobwekiva commented Jan 16, 2017

@TomasMikula My concern is that it adds a lot of noise to an otherwise simple idea, makes type-inference harder, and makes some of the user-facing methods harder to use because you have to supply them with additional type parameters (e.g. lift functions) :( Furthermore, you can make a Groupoid/Category instance only for some specific choice of L and H (since there is no GeneralizedGroupoid right now). And AFAIK scalaz is moving away from subtyping / variance.

Overall it feels like it's a marginal improvement with a non-trivial cost. The same constraints >: L <: H can always be supplied using a typeclass during construction (it doesn't have to be <~<, whatever is enough/necessary), e.g:

class X[A <: B](a: A)
// can be replaced by:
class X[A](a: A, ev: A <~< B)
// or if the evidence is not required after construction:
class X[A] private[X] (a: A)
object X {
  def apply[A](a: A)(implicit ev: A <~< B): X[A] = ???
}

<: bounds are basically poor man's typeclasses and can always be replaced by them. But that might require modifying an existing class.

What if we had BoundedLeibniz[L, H, A, B] / GeneralizedLeibniz[L, H, A, B] and Leibniz[A, B] wrapping it instead? That would solve all problems with some minor code duplication.

Another option is Leibniz[L, H, A, B] and ===[A, B] extends Leibniz[Nothing, Any, A, B] (or type ... =) and all methods on syntax traits.

Technically, Liskov can also be generalized along the same direction.

@TomasMikula
Copy link
Member

The same constraints >: L <: H can always be supplied using a typeclass

The problem is when the type constructor with type bounds is not under your control, so you can't change it. Do we say "f*** you" to all such use cases?

Aside: I think Scala's bounds checking is unnecessarily strict. If you have

class Foo[A <: Bar]

I believe you should be allowed to talk about Foo[Int] (it just might be uninhabited). This would make things simpler.

@hobwekiva
Copy link
Contributor Author

What do you think of the solutions I outlined at the end of my previous post? I am mostly concerned with type-inference on methods and combinators and I'd like to optimize for the most common case of Leibniz[Nothing, Any, A, B].

P.S.

Do we say "f*** you" to all such use cases?

Well, it is not like every single data type taking kind * -> * type parameters in Scalaz provides you a way to bound them with >: L <: H. Such constraints are unlikely to come up when you live in the Scalazzi world. Scalaz says "f*** you" to many other things like null, non-parametric equals, hashCode, exceptions, unconstrained side-effects, some of scala collections, (ought to say that to with intersection types) etc.

I believe you should be allowed to talk about Foo[Int] (it just might be uninhabited).

That would raise some interesting questions like:

trait Bar { type Out = String }
trait Baz { type Out = Boolean }
class Foo [A <: Bar] { type Out = A#Out }
type X = Foo[Baz]#Out // what is X?

@TomasMikula
Copy link
Member

What do you think of the solutions I outlined at the end of my previous post?

I like the one with wrapping.

Not sure how "all methods on syntax traits" helps if === is just a type alias (unless the methods are named differently anyway).

That would raise some interesting questions like:

trait Bar { type Out = String }
trait Baz { type Out = Boolean }
class Foo [A <: Bar] { type Out = A#Out }
type X = Foo[Baz]#Out // what is X?

Good point. For that the constraint would have to be checked.

@hobwekiva
Copy link
Contributor Author

This is roughly what I had in mind. Turns out there is quite a bit of duplication needed to completely avoid allocations (calling methods on BoundedLeibniz would require unwrapping and then wrapping Leibniz again). I also implemented LeibnizK there (useful for type-aligned sequences of natural transformations).

@TomasMikula
Copy link
Member

If Leibniz extended AnyVal, wrapping would be without allocation, right?

@S11001001
Copy link
Member

@TomasMikula Not quite.

Copy link
Member

@TomasMikula TomasMikula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of moving forward, I propose to merge this and add type bounds if/when needed.

@hobwekiva
Copy link
Contributor Author

Sorry for neglecting this PR, I've been experimenting with Leibniz, Liskov, and a number of other related ideas in a separate project. I understand why you want bounded version of Leibniz now.

@hobwekiva
Copy link
Contributor Author

hobwekiva commented Jun 4, 2017

Some of the new "inventions" and old goodies:

  • Is - unbounded Leibniz.
  • As - unbounded Liskov.
  • Leibniz - bounded Leibniz.
  • Liskov - bounded Liskov.
  • As1 - unbounded Liskov that uses existential types for its encoding, which allows you to call methods like def f[X <: B](x: X): F[X] given a: A and As1[A, B] and get F[A] back.
  • Liskov1 - ditto, but bounded.
  • IsK - higher-order Leibniz. I have tried poly-kinded Leibniz and Liskov (Ξ = AnyKind) and they are a lot of fun to play with, you can implicitly summon things like Some <~< Option and Monad <~< Functor.
  • IsF - equality of F-Bounded polymorphism types (use case gist).
  • Eq - experiments showed that you can make === even more powerful if you have an ability to lift term equality a === b to singleton type equality a.type === b.type (e.g. this gist, this one, and this one; the last gist also shows how to "prove" stuff in Scala, Idris-style). But you need an equality that satisfies forall (a: A) (b: A) (f: A -> Bool) . f(a) == f(b) <===> a === b assuming f follows Scalazzi rules.
  • Bounded - witnesses that L <: A <: H. Haven't really found the use so far.

I also found a way to emulate union types on Scala2 and I think it can be made safe but it is extremely mmm hacky in its appearance. Useful if you want to turn A <~< B into Liskov[L, H, A, B] given that you know that L <: A <: H and L <: B <: H.

@TomasMikula
Copy link
Member

If you want to replace this PR with your new Is, As, Leibniz, Liskov, IsK, I would give it a 👍.

experiments showed that you can make === even more powerful if you have an ability to lift term equality a === b to singleton type equality a.type === b.type (e.g. this gist, this one, and this one

In the last two gists there doesn't seem to be any guarantee that the types you are asserting to be equal are singleton types.

the last gist also shows how to "prove" stuff in Scala, Idris-style

This is nice. Could it be used to prove something about types that are not fully known at compile time? E.g. how would you prove that M + N = N + M when all you know at compile time is M <: Nat, N <: Nat?

But you need an equality that satisfies forall (a: A) (b: A) (f: A -> Bool) . f(a) == f(b) <===> a === b

I suppose you meant forall (a: A) (b: A) . (forall (f: A -> Bool) . f(a) == f(b)) <===> a === b.

@hobwekiva
Copy link
Contributor Author

hobwekiva commented Jun 5, 2017

If you want to replace this PR with your new Is, As, Leibniz, Liskov, IsK, I would give it a 👍.

Okay, awesome. I'll get it done this week.

In the last two gists there doesn't seem to be any guarantee that the types you are asserting to be equal are singleton types.

Right, those are early prototypes before I got into singleton types 😉

This is nice. Could it be used to prove something about types that are not fully known at compile time? E.g. how would you prove that M + N = N + M when all you know at compile time is M <: Nat, N <: Nat?

Well, ideally you would have the compiler (or a compiler plugin) figure out that since the proof is total and equality is a singleton (as in there is only one implementation according to free theorems), you can just "force" it for arbitrary M <: Nat and N <: Nat. I guess you could have some kind of

def assertTotal[P[_ <: Nat, _<: Nat]: Proof]
(proof: [M <: Nat, N <: Nat] (M, N) => P[N, M]): P[N, M]

I suppose you meant forall (a: A) (b: A) . (forall (f: A -> Bool) . f(a) == f(b)) <===> a === b).

Yep.

Conflicts:
	base/src/main/scala/scalaz/typeclass/Liskov.scala
	base/src/main/scala/scalaz/typeclass/LiskovFunctions.scala
	base/src/main/scala/scalaz/typeclass/LiskovInstances.scala
	base/src/main/scala/scalaz/typeclass/LiskovTypes.scala
Copy link
Member

@TomasMikula TomasMikula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, great stuff!

* explicitly coerce types. It is unsafe, but needed where Leibnizian
* equality isn't sufficient.
*/
def unsafeForce[A, B]: A === B =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this unsafeForce not need @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")), while all of the others do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scalaz doesn't use WartRemover as far as I can tell, I will simply remove this annotation. All asInstanceOf require it, I was just being lazy with warnings in leibniz 😉

// final def liftCt[F[-_]]: F[B] <~< F[A] = {
// type f[+α] = F[α] <~< F[A]
// substCo[f](refl)
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👎 for commented-out code.

@hobwekiva
Copy link
Contributor Author

hobwekiva commented Jun 20, 2017

It looks like CI checks failed because it couldn't download SBT.
EDIT: nvm all green now

@TomasMikula
Copy link
Member

Sorry for waiting so long, I just wanted to give others the chance to review. If you find time to resolve the conflicts, I will merge this. If not, I will try to resolve them myself. Thanks!

@hobwekiva
Copy link
Contributor Author

#1524

@hobwekiva hobwekiva closed this Nov 22, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants