Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@adamgfraser
Copy link
Contributor

Currently our laws functionality can support laws for type classes such as Equal that are parameterized on a concrete type but not for type classes that are parameterized on higher kinded types such as Zippable.

To address this, this PR begins by implementing a GenF[R, F[_]], which represents a way of creating a generator of values of type F[A] given a generator of values of type A. Note that we need to create a new trait for this because we need to universally quantify over A.

We then say that a ZLawsF[R, F] knows how to, given a way of lifting generators of concrete values into the target type, return a test result. A variety of convenience methods are provided for creating these analogous to the ones for ZLaws.

I included an example of using this functionality to test an instance for a covariant functor.

One further complication is that so far our laws have always been generated from functions of one or more values of the original type. But for the composition law we need to generate functions as well. I implemented two proposed alternatives. The first is very generic, allowing the user to return a test result from a set of generic generators, but requires more work from the laws author. The second just adds a specialized constructor for this "One F[A], two functions" case. This is extremely ergonomic for the writer of the laws but more specialized. Still, there are probably not that many common pattern in laws.

There may be an intermediate version where the user provides a function from arbitrary generators and a GenF to the generators they need, and then a function from the values returned by those generators to a test result.

@adamgfraser adamgfraser requested a review from jdegoes April 9, 2020 21:43
import zio.test.Gen

trait GenF[-R, F[_]] {
def apply[R1 <: R, A](gen: Gen[R1, A]): Gen[R1, F[A]]
Copy link
Member

Choose a reason for hiding this comment

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

This is really nice!

Copy link
Member

Choose a reason for hiding this comment

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

Possibly this is another place where we might need to propagate the instances required of A.

Naively:

trait GenF[-R, F[_], Caps[_]] {
   def apply[R1 <: R, A: Caps](gen: Gen[R1, A]): Gen[R1, F[A]]

I'm thinking of the situations where, in order to create F[A], the A itself must satisfy some properties.

Or maybe there's a simpler way.

object Covariant extends LawfulF.Covariant[Covariant] {

val identityLaw = new LawsF.Covariant.Law1[Covariant]("identityLaw") {
def apply[F[+_]: Covariant, A](fa: F[A]): TestResult = {
Copy link
Member

Choose a reason for hiding this comment

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

In general in order to perform these checks, we would require type class instances for the parametric A, such as Equal, for example. This to me suggests maybe Law1 etc should be parameterized by two things:

The higher-kinded type class (in this example, Covariant), and the set of type classes that must be satisfied by the parametric types (e.g. Equal).

Although that does raise other complications, such as where do these types come from. If the user has control of the type classes, then they also need to have control over the "poly" gens for the types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I was thinking about that as well. I think the way we solve it is with EqualF, which gives us a way to determine whether two F[A] values are equal given a way to determine whether two A values are equal. I don't think it is enough just to have a way to determine whether two A values are equal because we don't know how to compare the F[A] values. We could implement this similarly to how we did GenF.

Copy link
Member

Choose a reason for hiding this comment

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

I do think we need EqualF and friends.. then we can compare the F[A] given an Equal: A. But where will we get that Equal[A]? The function is polymorphic in A so has no structure on A. So it seems we need them both: propagating capabilities on the types down, but also polymorphic basic type classes like EqualF`.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Ideally from the perspective of the caller we would like to remain fully polymorphic in the A type. Potentially this aspect of the functionality could best be provided along with the library that defines the type classes. Then we could say "the law has to operate on abstract types A, B, and C and the only guarantee we will give you is they have this structure". And then under the hood the test framework just uses something like integers that have all this structure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you are right. The downside of that is I think that means we will need the caller to provide us with a generator of A values with the required capabilities. We will then take it and hide the actual type in everything we do but we need some way to generate values with the required capabilities.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, agreed. And it's unfortunate, but it kind of makes sense, if you think about constrained functors, for example, you can only map them to specify types (e.g. types that are serializable), so the user is going to have to ultimately provide the gens for values that have the required properties, but like you say, we can hide it in the actual laws.

I briefly thought about having something like G <: GenPoly. If you propagated that in sufficient places, you could put the type class instances in there. Maybe not quite as nice to use from a syntactic perspective, because the instances aren't implicit, but maybe better integrated with the GenPoly machinery.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that is definitely something we could explore.

* Constructs a law from a pure function taking two parameters.
*/
abstract class Law2[-Caps[_[+_]]](label: String) extends Covariant[Caps, Any] { self =>
def apply[F[+_]: Caps, A, B](fa: F[A], fb: F[B]): TestResult
Copy link
Member

Choose a reason for hiding this comment

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

I wonder what all the tradeoffs are between F[A] and F[B], versus two F[A]. Probably n different types is fine, as long as they all satisfy the same instances required by the laws.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this implementation gives us the best of both worlds because at least right now all the underlying types are integers but the users and authors of laws aren't allowed to know anything about the types. So I think it makes sense to have them all be different to force the user to put the types together in the right way.


object ZLawsF {

sealed trait Covariant[-Caps[_[+_]], -R] { self =>
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we end up with something like:

Suggested change
sealed trait Covariant[-Caps[_[+_]], -R] { self =>
sealed trait Covariant[-CapsF[_[+_]], -Caps[_], -R] { self =>

So you can do things like Covariant[Foldable, Equal, Any], which would say, my Foldable laws can operate on any A for which Equal is defined.

Or maybe there's a simpler way.

@adamgfraser
Copy link
Contributor Author

@jdegoes Thanks for the comments! I think I have a good direction on this. Will come back with another revision shortly.

@adamgfraser
Copy link
Contributor Author

@jdegoes Updated to support required capabilities for the parameter type.

@jdegoes
Copy link
Member

jdegoes commented Apr 12, 2020

I think it's ready for us to test it out, and if we find any problems downstream, we can always tweak it more at this point. Thanks for your work on this!

@jdegoes jdegoes merged commit a109e83 into zio:master Apr 12, 2020
@adamgfraser adamgfraser deleted the laws branch April 13, 2020 15:01
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.

2 participants