-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conditional (restricted) type class instances #3
Comments
I like the idea of Also a thought on ideas like this: if we'll find a solution that is not breaking and can be added later, after the merge, I would rather don't include it in the big PR. That PR is going to be controversial, and all features like this will make it even more so. This might slow down the process. On the other hand, if we find a better solution, but a breaking one. Maybe we should include it, to have all breaking changes in a single version update. |
This may or may not be relevant to the proposal, but to support built-in types one should use |
@davidchambers I simplified the examples to exclude builtins for illustrative purposes. The idea remains the same. |
Sorry, I didn't explain that properly at all. I'll reply in this thread as that seems to be a more appropriate place. I think the approach I suggested in #2 (comment) can also solve the problem in this issue. Maybe.Just = a => ({
value: a,
tag: 'just',
get ["fantasy-land/canonical"]() {
return {
concat: isSemigroup(a) ? impl : null
};
}
}); In other words, if an implementation needs a dynamic module then it can use a getter and have complete freedom. An alternative way would be to do: Maybe.Just = a => ({
value: a,
tag: 'just',
'fantasy-land/canonical': {
concat: isSemigroup(a) ? impl : null
}
}); Which is very close to the original example. I'm curious what you think about that @Avaq? |
Ah, now I understand. Although this would be against following requirement from specification:
This basically means that Also I can't remember why I've added this requirement at all 😅 |
Ok, so without this requirement following would be allowed:
This will make generic code unreliable. For example function map(f, v) {
return v['fantasy-land/canonical'].map(f, v)
}
function lift2(f, v1, v2) {
const A = v1['fantasy-land/canonical']
return A.ap(A.map(x => y => f(x, y), v1), v2)
} Although I see that this requirement blocks a particular technique to implement restricted type class instances. The one when we enforce type safety by removing certain methods. But maybe it's not a good idea to enforce type safety like that. For example here it won't work anyway: const a = Maybe.of(List(1, 2, 3))
const b = Maybe.of(Text('123'))
if (a['fantasy-land/canonical'].concat && b['fantasy-land/canonical'].concat) {
a['fantasy-land/canonical'].concat(a, b) // Boom!
} In this example we would probably get an exception form |
@rpominov I think you have a lot of good observations @rpominov.
As far as I can see the other solution that relied on
I think something needs to done to improve the rule while still disallowing those things. The rule is a bit vague as of right now. For instance, it's a unclear what "values" exactly means in "that module must produce values with references to itself." We want to say that
In this specific example, doesn't the functor law prevent that? |
No, we could have
Good point, would be great to clarify this. Sometimes it's hard to manage balance between strictness and clarity. When you try to write something 100% correctly, you end up with a simple concept described in a complex way. Maybe we could rely on common sense here?
Algebra laws currently only concerned with modules, and don't say anything about |
True, but that seems to introduce unnecessary complexity when the value could instead just only provide the functions that it actually supports. It seems confusing to at the same time have a
How so? The first functor law states that
The more I think about it the more I agree with this sentiment. Besides the problem you mention there is another problem as well. Let's say you want to create a
So it seems to me that the current specification doesn't allow for such "dynamic methods" either. |
But in my example it was arbitrary function |
The approach we took with Sanctuary is to define the method conditionally: // Add "fantasy-land/concat" method conditionally so that Just('abc')
// satisfies the requirements of Semigroup but Just(123) does not.
if (this.isNothing || Z.Semigroup.test(this.value)) {
this['fantasy-land/concat'] = Maybe$prototype$concat;
} This ensures that |
But the functor doesn't know if it is given the identity function or some other function. That's part of what makes that law so powerful. If the functor cannot mess with the canonical module when given the identity function then it cannot mess with the canonical module when given any function.
My point was exactly that that approach does not work with the current spec. Consider this example. const a = S.Just(123);
const b = a["fantasy-land/map"](_) => "abc"); If
So the approach is not compliant with the spec. When I map over a |
Did you mean to use |
@davidchambers I don't think so? Does is not make sense? |
Oh, I think I see your point now. Are you showing that it's problematic for |
Yes, that is exactly what I meant 😄 |
Cool. It just took me a while to catch up with you. :) I don't have a problem with breaking the letter of the law (by inspecting the value passed to |
Is it better though? If programmer made a mistake and wrote something like |
@davidchambers I've probably tested an older version of sanctuary before (found some REPL online), just tested with the latest version. First of all, man, these error messages are the best!
Seems like you handle all cases I was worried about perfectly. Although I've noticed that it seems like you don't need to remove the |
I appreciate the positive feedback, @rpominov. The error messages are not nearly as friendly as Elm's (sanctuary-js/sanctuary-def#150), but they're usually quite informative.
Prior to sanctuary-js/sanctuary#359, Sanctuary would permit By attaching S.concat (S.Just (1));
// ! Type-class constraint violation
//
// concat :: Semigroup a => a -> a -> a
// ^^^^^^^^^^^ ^
// 1
//
// 1) Just(1) :: Maybe Number, Maybe FiniteNumber, Maybe NonZeroFiniteNumber, Maybe Integer, Maybe NonNegativeInteger, Maybe ValidNumber
//
// ‘concat’ requires ‘a’ to satisfy the Semigroup type-class constraint; the value at position 1 does not.
//
// See https://github.com/sanctuary-js/sanctuary-type-classes/tree/v7.1.1#Semigroup for information about the sanctuary-type-classes/Semigroup type class. |
@davidchambers would it be possible to provide same error early by looking at the type I just realized another issue we'll face if we allow module to change from value to value. We wouldn't be able to reuse a module. For example we would have to rewrite this function lift2(f, v1, v2) {
const A = v1['fantasy-land/canonical']
return A.ap(A.map(x => y => f(x, y), v1), v2)
} to something like this: function lift2(f, v1, v2) {
const mapped = v1['fantasy-land/canonical'].map(x => y => f(x, y), v1)
return mapped['fantasy-land/canonical'].ap(mapped, v2)
} Also in case when a method takes more than one value we would need to somehow decide which canonical module to use e.g., do I do Alternative solution suggested by @Avaq solves this issue. |
The term type class is a misnomer in the Sanctuary world. We don't really have types, so we can't make groups of types. We do have values, though, and we can fake type classes by inspecting values (as well as making allowances for values of built-in “types”). |
The way I would write the following instance:
with a static-land like approach would be: const Maybe = (() => {
// ADT
const Nothing = { hasValue: false };
const Just = x => ({ hasValue: true, x });
const match = ({ Nothing, Just }) => ({ hasValue, x }) =>
hasValue ? Just(x) : Nothing;
// We don't try to put all possible instances in here, just the ones that apply to all Maybes, like
const of = Just;
const chain = f => match({ Nothing, Just: f });
return { Nothing, Just, match, of, chain };
})();
// Destructure for convenience
const { Nothing, Just, match } = Maybe;
// The universe of instances is open, and you can always write more dictionaries
// instance (Semigroup s) => Monoid (Maybe s) where ...
const Maybenoid = S => ({
empty: Nothing,
concat: match({
Nothing: y => y,
Just: x =>
match({
Nothing: Just(x),
Just: y => Just(S.concat(x)(y))
})
})
});
const IntSum = { empty: 0, concat: x => y => x + y }
console.log(Maybenoid(IntSum).concat(Just(1))(Just(2))) // => Just(3)
console.log(Maybenoid(IntSum).concat(Nothing)(Just(2))) // => Just(2) |
There's other ways to have a const Fn = { id: x => x, flip: f => a => b => f(b)(a) };
// Handy for flipping semigroups around
const Dual = M => ({ ...M, concat: Fn.flip(M.concat) });
// the First semigroup just drops the second value
const First = { concat: x => y => x };
// the Last semigroup does the opposite
const Last = Dual(First);
const Arr = (() => {
const foldMap = M => f => arr =>
arr.reduce((p, c) => M.concat(p)(f(c)), M.empty);
const fold = M => foldMap(M)(Fn.id);
return { foldMap, fold };
})();
// Both of them can be plugged into Maybenoid to get interesting instances
const tests = [
Arr.fold (Maybenoid(First)) ([]), // => Nothing
Arr.fold (Maybenoid(First)) ([Just(1), Nothing, Just(2)]), // => Just(1)
Arr.fold (Maybenoid(Last)) ([]), // => Nothing
Arr.fold (Maybenoid(Last)) ([Just(1), Nothing, Just(2)]) // => Just(2)
];
console.log(tests); |
In Haskell, you can implemented an instance for a subset of values, by restricting the type variable, like so:
In JavaScript, using Fantasy Land, we've taken to use conditionally assigned properties to achieve a similar effect, eg:
I believe that switching from individually assigned properties that each correspond to a single algebra, to using a single property that points to a mapping of all implemented algebra's would make this practice less convenient at the least.
Now for some thoughts on possible solutions. It makes sense to do something like this:
But with this approach, it's not possible to detect beforehand whether value that use
Maybe
as their canonical module actually have an instance of Semigroup, which is very useful for run-time type checking. So perhaps this calls for keeping this data available somehow, for example:I'm also curious how one would write TypeScript definitions for these restricted instances, but I don't know enough about it.
The text was updated successfully, but these errors were encountered: