Fantasy-land: require static methods to be defined on type representatives

Created on 25 Sep 2016  Â·  2Comments  Â·  Source: fantasyland/fantasy-land

_I spent a long time writing a lot of words, but I've decided to delete many of them and start with an example. Hopefully this makes the problem clear._


Let's use JSVerify to define a generator for possibly empty values of any type which satisfies both Monoid and Applicative:

//    ApplicativeMonoidArb :: (Applicative m, Monoid (m a)) =>
//                              { fantasy-land/empty :: () -> m a } ->
//                              { fantasy-land/of :: a -> m a } ->
//                              Arbitrary a ->
//                              Arbitrary (m a)
const ApplicativeMonoidArb = $empty => $of => arb =>
  jsc.oneof(jsc.constant($empty['fantasy-land/empty']()),
            arb.smap($of['fantasy-land/of']));

Usage example:

//    MaybeArb :: Arbitrary a -> Arbitrary (Maybe a)
const MaybeArb = ApplicativeMonoidArb(S.Maybe)(S.Maybe);

MaybeArb could then be used to generate values for property-based testing:

jsc.assert(jsc.forall(MaybeArb(jsc.string), m => {
  //  Assert that ‘m’ satisfies various laws...
}));

Why are { fantasy-land/empty :: () -> m a } and { fantasy-land/of :: a -> m a } separate? (It forces us to provide S.Maybe twice, which is tedious.) The reason is that the spec allows this:

{ constructor :: { fantasy-land/empty :: () -> Maybe a }
, fantasy-land/of :: Maybe a ~> b -> Maybe b
, ...
}

As a result there's no guarantee that there exists a "record" with both fantasy-land/empty and fantasy-land/of fields. :(

Defining MaybeArb was straightforward, but let's also consider the case in which we're given a value x of type (Applicative m, Monoid (m a)) => m a from which we'd like to generate other values. How would we use ApplicativeMonoidArb in this case? It's not pretty:

//    XArb :: Arbitrary a -> Arbitrary (X a)
const XArb = ApplicativeMonoidArb(typeof x['fantasy-land/empty'] === 'function' ? x : x.constructor)(typeof x['fantasy-land/of'] === 'function' ? x : x.constructor);

This is our current model:

+------------------+                     +------------------+
|                  |                     |                  |
|      t :: T      | --> constructor --> |        ??        |
|                  |                     |                  |
+------------------+                     +------------------+
   |                                        |
   +--> fantasy-land/empty .... AND/OR .... +--> fantasy-land/empty
   |                                        |
   +--> fantasy-land/concat                 |
   |                                        |
   +--> fantasy-land/map                    |
   |                                        |
   +--> fantasy-land/ap                     |
   |                                        |
   +--> fantasy-land/of ....... AND/OR .... +--> fantasy-land/of

I propose that we adopt this model:

+------------------+                     +------------------+
|                  |                     |                  |
|      t :: T      | --> constructor --> |  T :: TypeRep T  |
|                  |                     |                  |
+------------------+                     +------------------+
   |                                        |
   +--> fantasy-land/concat                 +--> fantasy-land/empty
   |                                        |
   +--> fantasy-land/map                    +--> fantasy-land/of
   |
   +--> fantasy-land/ap

How does this differ from the current model?

  • Everything is defined in exactly one place.
  • Static methods are clearly separated from instance methods.
  • Rather than ?? which may or may not have some useful properties we have a type representative with known properties.

How would our example look in this world?

//    ApplicativeMonoidArb :: (Applicative m, Monoid (m a)) => TypeRep m -> Arbitrary a -> Arbitrary (m a)
const ApplicativeMonoidArb = typeRep => arb =>
  jsc.oneof(jsc.constant(typeRep['fantasy-land/empty']()),
            arb.smap(typeRep['fantasy-land/of']));

This type signature is much clearer!

Updated usage example (given a type representative):

//    MaybeArb :: Arbitrary a -> Arbitrary (Maybe a)
const MaybeArb = ApplicativeMonoidArb(S.Maybe);

We need only provide S.Maybe once now. :)

Updated usage example (given a value):

//    XArb :: Arbitrary a -> Arbitrary (X a)
const XArb = ApplicativeMonoidArb(x.constructor);

Where did all our ternaries go? ;)


Now that we have prefixed method names and ap is consistent with map and chain, I believe the {empty,of,chainRec} "schizophrenia" to be the most important unresolved issue. It has already been discussed a great deal: #16, #88, #97, #158, #162, #163, #164, sanctuary-js/sanctuary-type-classes#3. It would be wonderful to agree on a solution (which may or may not be the one proposed above) and release an update to the spec.

Most helpful comment

Yes, that's what I have in mind, @safareli.

All 2 comments

So we could state this:

someFunc :: (ChainRec m, Monoid m, Applicative m) => TypeRep m -> SomeStructure a -> m a

And here TypeRep m would represent dictionary like this: ?

{ "fantasy-land/empty": () -> m a
, "fantasy-land/of": a -> m a
, "fantasy-land/chainRec": ((a -> c, b -> c, a) ->  m c,  a) -> m b
}

Yes, that's what I have in mind, @safareli.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

davidchambers picture davidchambers  Â·  15Comments

wenkanglin picture wenkanglin  Â·  4Comments

joneshf picture joneshf  Â·  8Comments

safareli picture safareli  Â·  16Comments

puffnfresh picture puffnfresh  Â·  6Comments