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

Adds an As class which represents subtyping relationships (Liskov) #1564

Closed
wants to merge 13 commits into from

Conversation

stew
Copy link
Contributor

@stew stew commented Mar 18, 2017

This is a direct port of the Liskov class from scalaz. I named it As to be
similar to our new Is class representing type equality, but there are aliases
named <~< and Liskov

This is a direct port of the Liskov class from scalaz.  I named it `As` to be
similar to our new `Is` class representing type equality, but there are aliases
named `<~<` and `Liskov`
* to a function, such as F in: `type F[-B] = B => String`. In this
* case, we could use A As B to turn an F[B] Into F[A].
*/
def subst[F[-_]](p: F[B]): F[A]
Copy link
Contributor

Choose a reason for hiding this comment

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

We use substitute in Is.

* The original contribution to scalaz came from Jason Zaugg
*/
sealed abstract class As[-A, +B] {
def apply(a: A): B = As.witness(this)(a)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we called this coerce in Is. Can we use the same for both?

/**
* Use this relationship to widen the output type of a Function1
*/
def onF[X](fa: X => A): X => B = As.co2_2[Function1, B, X, A](this)(fa)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this one on the instance but generally the rest are on the object?

/*
* Subtyping forms a category
*/
implicit val liskov: Category[<~<] = new Category[<~<] {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we have a category for Is. we should be consistent.

/**
* Subtyping is reflexive
*/
implicit def refl[A]: (A <~< A) =
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this and isa?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, in Is we use a singleton to avoid allocations.

new (A <~< A) {
def subst[F[-_]](p: F[A]): F[A] = p
}

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have a method to materialize a <:< as needed. I think we should. Also a similar unsafe method to go from <:< to As.

def co2[T[+_, _], Z, A, B](a: A <~< Z): T[A, B] <~< T[Z, B] =
a.subst[λ[`-α` => T[α, B] <~< T[Z, B]]](refl)

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

If we have all these methods below I think we should have the analogous for Is.

@@ -2,4 +2,24 @@ package cats

package object evidence {
type Leibniz[A, B] = cats.evidence.Is[A, B]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we use cats.evidence here but not below?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also add a === alias to parallel <~<?

/**
* Unsafely force a claim that A is a subtype of B
*/
def force[A, B]: A <~< B =
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should have this. You can call isa and cast if you want to do something unsafe. The cast at least shows you it is unsafe. Burying an unsafe cast in a library function seems like a bad choice to me.

stew added 3 commits March 19, 2017 01:19
remove explicit reference to package in Is type alias (for posco)
add === type alias for `Is` (for sellout)
* Lift Scala's subtyping relationship
*/
@inline def unsafeFromPredef[A, B >: A]: (A As B) =
reflAny.asInstanceOf[A As B]
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is actually safe (you could allocate A As B here). It is going from A <:< B that I think is correct but can't be proven via the types.

Copy link
Contributor

Choose a reason for hiding this comment

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

So, start with refl, then do to covariance you have A As A <: A As B so it can be returned. So I think returning refl[A] here will compile.


class AsTests extends CatsSuite {
import evidence._

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we test resolution explicitly of a few types? Int As Any (Int, Long) As (Any, Any)

stew added 3 commits March 25, 2017 11:04
- change unsafeFromPredef to just use refl
- add tests for some expected relationships such as Int <~< Any,
  and List[String] <~< List[Any]
@stew
Copy link
Contributor Author

stew commented Apr 3, 2017

@johnynek I think I've covered all your requests now

@sir-wabbit
Copy link

sir-wabbit commented Apr 4, 2017

I think unsafeFromPredef should be renamed to isa or reify and implemented in terms of refl. True unsafeFromPredef should have signature A <:< B => A <~< B (it's actually safe AFAIU, just uses a cast).

  implicit def reify[A, B >: A]: A <~< B = refl[A]

  def fromPredef[A, B](eq: A <:< B): A <~< B =
    unsafeForce[A, B]

@johnynek
Copy link
Contributor

johnynek commented Apr 4, 2017

I agree with @alexknvl 's comments. I don't see how what we have currently called unsafeFromPredef is unsafe. If it is, can you comment?

Also, the coverage of this PR is pretty low. I love using this:

https://chrome.google.com/webstore/detail/codecov-extension/keefkhehidemnokodkdkejapdgfjmijf?hl=en-US

to see which methods have no tests. Very helpful (even a fun game to get everything green).

@sellout
Copy link
Contributor

sellout commented Apr 4, 2017

I think this is the last thing that needs to be merged to get a Monocle-Cats PR submitted.

@kailuowang kailuowang added this to the cats-1.0.0 milestone Apr 10, 2017
@codecov-io
Copy link

codecov-io commented Apr 16, 2017

Codecov Report

Merging #1564 into master will increase coverage by 0.85%.
The diff coverage is 92.3%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1564      +/-   ##
==========================================
+ Coverage   93.08%   93.93%   +0.85%     
==========================================
  Files         250      242       -8     
  Lines        3989     4122     +133     
  Branches      136      161      +25     
==========================================
+ Hits         3713     3872     +159     
+ Misses        276      250      -26
Impacted Files Coverage Δ
core/src/main/scala/cats/evidence/As.scala 92.3% <92.3%> (ø)
core/src/main/scala/cats/ApplicativeError.scala 86.66% <0%> (-13.34%) ⬇️
core/src/main/scala/cats/syntax/cartesian.scala 75% <0%> (-10.72%) ⬇️
core/src/main/scala/cats/Applicative.scala 75% <0%> (-8.34%) ⬇️
core/src/main/scala/cats/Reducible.scala 93.22% <0%> (-1.61%) ⬇️
laws/src/main/scala/cats/laws/discipline/Eq.scala 84.37% <0%> (-0.25%) ⬇️
core/src/main/scala/cats/instances/list.scala 100% <0%> (ø) ⬆️
core/src/main/scala/cats/syntax/foldable.scala 100% <0%> (ø) ⬆️
core/src/main/scala/cats/data/StateT.scala 97.43% <0%> (ø) ⬆️
core/src/main/scala/cats/data/OptionT.scala 100% <0%> (ø) ⬆️
... and 58 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 19c3a37...3954ec7. Read the comment docs.

stew added 2 commits April 16, 2017 15:00
… derrived from what we have, and I can't find anyone using them in the wild)
@sir-wabbit
Copy link

sir-wabbit commented Apr 22, 2017

May I also suggest not adding Liskov alias? I think it might be a good idea to reserve some name space in case bounded versions of Is and As are added (they could be named Leibniz and Liskov respectively).

@kailuowang
Copy link
Contributor

kailuowang commented May 17, 2017

@stew it looks to me that, luckly, the compilation error on scalajs on 2.10.6 can be fixed by upgrading scalajs to 0.6.16, which is the version on master. So if you merge in master, it should probably build. UPDATE: my user error, it's still broken on scalajs 0.6.16 on 2.10.6
@johnynek it looks like that the test coverage is decent now. Wanna take another look?

@kailuowang
Copy link
Contributor

kailuowang commented May 19, 2017

@stew I found out the cause, the type inference in toMap lin AsTests line 7 is a bit too much for scalajs on 2.10.6 to handle. A little help like the following gets it pass.

  def toMap[A, B, X](fa: List[X])(implicit ev: X <~< (A,B)): Map[A,B] = {
    type RequiredFunc = (Map[A, B], X) => Map[A, B]
    type GivenFunc = (Map[A, B], (A, B)) => Map[A, B]
    val subst: GivenFunc <~< RequiredFunc = As.contra2_3(ev) //introduced because inference failed on scalajs on 2.10.6
    fa.foldLeft(Map.empty[A,B])(subst(_ + _))
  }

Do you have time to update the PR with this change? @johnynek do you mind taking another look?

Thank you very much @kailuowang for the solutiongit diff
@kailuowang kailuowang mentioned this pull request May 25, 2017
26 tasks
val f2: Any => Top = As.onF(cAsA)(f)
}

test("we can simultaneously narrow the input and widen the ouptut of a Function1") {
Copy link
Contributor

@kailuowang kailuowang May 31, 2017

Choose a reason for hiding this comment

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

looks like we are missing a test here?

@kailuowang
Copy link
Contributor

@stew got a bit time to finish this up? If not I would be more than happy to work on it.

@kailuowang
Copy link
Contributor

@johnynek I continued this PR with a new one #1728. cc @sellout

kailuowang added a commit that referenced this pull request Jun 14, 2017
* Adds an As class which represents subtyping relationships

This is a direct port of the Liskov class from scalaz.  I named it `As` to be
similar to our new `Is` class representing type equality, but there are aliases
named `<~<` and `Liskov`

* incorporate suggestions from Oscar (Thanksgit diff --cached)git

* delete-trailing-whitespace

* Minor changes to evidence/packages.scala

remove explicit reference to package in Is type alias (for posco)
add === type alias for `Is` (for sellout)

* More enhancements for posco's feedback (thanks!)

- change unsafeFromPredef to just use refl
- add tests for some expected relationships such as Int <~< Any,
  and List[String] <~< List[Any]

* ome more

* add more tests, contravariant tests still pending

* add contra tests and get rid fo the liftF like functions (they can be derrived from what we have, and I can't find anyone using them in the wild)

* don't try to link the type in the scaladoc

* fix test failure on 2.10.6 + scalajs

Thank you very much @kailuowang for the solutiongit diff

* added helper methods for converting Function1

* added category law tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants