_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?
?? 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.
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.
Most helpful comment
Yes, that's what I have in mind, @safareli.