-
-
Notifications
You must be signed in to change notification settings - Fork 95
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
Change type validators to use predicate instead of TypeRep #191
Conversation
@@ -2341,25 +2341,24 @@ | |||
//. ``` | |||
S.lastIndexOf = sanctifyIndexOf('lastIndexOf'); | |||
|
|||
//# pluck :: Accessible a => TypeRep b -> String -> [a] -> [Maybe b] | |||
//# pluck :: Accessible a => (b -> Boolean) -> String -> [a] -> [Maybe c] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super happy with the type signatures. Both b
and c
are "unique type variables", so they may as well be Any
. I wish there was some way to indicate inside the type signature that c
is the set of values that pass the predicate. Perhaps a Predicate a
pseudotype?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a
Predicate a
pseudotype?
I'm not sure how this would work. Could you provide the type signature you have in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea would be that Predicate a
somehow constrains a
to be the set of values for which the predicate function of type Predicate a
returned true.. It doesn't quite work like that I think, but I wish it could.
1331577
to
c30cbbd
Compare
A major concern I have about this change is the ease of upgrading. Since many TypeReps are actually also Functions, the type checker will not complain. Take the following example: S.get(Array, 'x', {x: 1}) Before this commit, this would return A half-solution would be to change filter to be more strict about the output of predicates: // filter :: (Monad m, Monoid m) => ((a -> Boolean), m a) -> m a
var filter = function(pred, m) {
return m.chain(function(x) {
- return pred(x) ? m.of(x) : m.empty();
+ return pred(x) === true ? m.of(x) : m.empty();
});
}; But this would just make the function always return It would be somewhat better if we asserted the output of the predicate to be a One idea is to use the
|
@Avaq, could you point to the dc-higher-order-functions branch of sanctuary-def and try the |
c30cbbd
to
46ba190
Compare
I rebased this on |
I just rebased that branch for you. :) |
@@ -279,6 +279,9 @@ | |||
var l = $.TypeVariable('l'); | |||
var r = $.TypeVariable('r'); | |||
|
|||
// Predicate :: Type | |||
var Predicate = $._UnaryFunction($.Any, $.Boolean); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure where to put this, nor how to signature it, but I like it being separated because;
- It shortens the type-signatures it's used in to below 79 characters.
- Less repitition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me, since in each case the predicate must accept any value. For a function such as filter
, though, we'd want a -> Boolean
rather than Any -> Boolean
.
Seen that |
No. That's new to me. |
It was an intermittent (network?) error, apparently. Tests are now passing (after rebuilding). :) |
I'm in the process of rebasing this because it's on the to-do list for the next release. But it may take a while; a lot has changed. |
That's exciting news, @Avaq! Thank you. :) |
b591bb2
to
938f8e2
Compare
I've made the rebase. Several things have changed:
|
938f8e2
to
9a90a49
Compare
Doc tests didn't run in 7.0. Fixing docs. |
3a95cc0
to
e3ea415
Compare
Allow me to extend upon this statement: The reason we didn't merge just came back to me. It was this concern I had about Perhaps we should hold off until #232 is merged. Or; knowing that it will be merged before the next release, it should be safe to merge this and upgrade the |
eq(S.get(String, 'x', {x: 0, y: 42}), S.Nothing); | ||
eq(S.get(S.K(true), 'x', {x: 0, y: 42}), S.Just(0)); | ||
eq(S.get(S.K(true), 'y', {x: 0, y: 42}), S.Just(42)); | ||
eq(S.get(S.K(true), 'z', {x: 0, y: 42}), S.Just(undefined)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behavior was discussed with @davidchambers on Gitter, and he noted that he would prefer this to return Nothing
(by determining beforehand whether the object property even exists). This has several implications:
- The behavior will change from how it currently works. Currently; if we were to provide a
TypeRep Nil
, we would correctly getJust(undefined)
. The only reason we don't come across this is becauseTypeRep Nil
does not exist. - We will have to make a decision on what constitutes existence of an object property, eg. do we walk the prototype chain? Currently we delegate all of the decision making to the type identifier.
- I'm concerned that behavior might be unexpected. If I provide a type identifier which includes
undefined
as one of its members (eg.Nil | String
) then it would behave the same as the type excludingundefined
(eg.String
) for JSON parsed data. Myget
would not resolve toJust(undefined)
, despiteundefined
being an expected type.
What are your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this?
function get(pred, key, obj) {
var x = obj[key];
return key in obj && pred(x) ? Just(x) : Nothing;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I draw a distinction between {x: undefined}
and {}
. In the Sanctuary world we should never think the result of ({}).x
is undefined
. Instead, we should think the result of indiscriminately getting the value of the x
property of {}
is Nothing
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood. Your reasoning makes sense, yet my concern still stands. I will make the change, and I'll have to see how it plays out in practice once it's released. It's a minor concern anyway. :)
var maybe = keys.reduce(function(m, k) { | ||
return m.chain(function(x) { return x == null ? Nothing : Just(x[k]); }); | ||
return m.chain(function(x) { | ||
return x != null && k in x ? Just(x[k]) : Nothing; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to negate the null
check rather than negating the k in x
check for aesthetic reasons. Means that the false case in now on the right hand side of the ternary.
var S = require('..'); | ||
|
||
var vm = require('vm'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert this change, please. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚡ (merge error I missed)
|
||
eq(S.gets(RegExp, ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/)); | ||
eq(S.gets(vm.runInNewContext('RegExp'), ['x'], {x: /.*/}), S.Just(/.*/)); | ||
eq(S.gets(S.K(true), ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use S.is(RegExp)
here rather than S.K(true)
, and reinstate the second assertion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured S
would be undefined in a new context. But I haven't asserted that. Let me check. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed; ReferenceError: S is not defined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I had in mind:
- eq(S.gets(RegExp, ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
- eq(S.gets(vm.runInNewContext('RegExp'), ['x'], {x: /.*/}), S.Just(/.*/));
+ eq(S.gets(S.is(RegExp), ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
+ eq(S.gets(S.is(vm.runInNewContext('RegExp')), ['x'], {x: /.*/}), S.Just(/.*/));
This way we'd continue to assert that a regexp from one environment is a member of the type represented by RegExp
from another environment. Admittedly this is really testing is
rather than gets
, but that's okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh. Right. ⚡
|
||
throws(function() { S.gets(Number, null); }, | ||
throws(function() { S.gets(S.K(true), null); }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use S.is(Number)
here (and below).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for the get
tests? Should I change all S.K(Bool)
functions to appropriate S.is(TypeRep)
functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please. It's nice for the diff to act as an upgrade guide as much as possible. :)
return filter(function(x) { return is(typeRep, x); }, | ||
encase(JSON.parse, s)); | ||
function parseJson(pred, s) { | ||
return filter(function(x) { return pred(x); }, encase(JSON.parse, s)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function(x) { return pred(x); }
is better written pred
. 😜
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good point. I'll change it in both places.
var x = obj[key]; | ||
return is(typeRep, x) ? Just(x) : Nothing; | ||
return key in obj && pred(x) ? Just(x) : Nothing; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't attempt obj[key]
before the key in obj
check. Let's replace the two lines above with the following:
return key in obj && pred(obj[key]) ? Just(obj[key]) : Nothing;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we want to optimise property access and repetition we can do this:
var x = null
return key in obj && (x = obj[key], pred(x) ? Just(x) : Nothing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose it's cleaner that way. Though perhaps it would then be best to wrap the whole statement in an if
. When implementing Promise/A+ I had to access the .then
property once and only once, because property access in JavaScript is impure due to custom getters.
EDIT: Or the suggestion by Irakli over an if
statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind too much. I'll leave it to you, Aldwin. If you go with Irakli's approach, we could avoid using the comma operator:
var x;
return key in obj && pred(x = obj[key]) ? Just(x) : Nothing;
This approach seems okay too:
if (key in obj) {
var x = obj[key];
if (pred(x)) return Just(x);
}
return Nothing;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@safareli Your statement returns false
rather than Nothing
for non-existent properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚡
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Avaq you are right.
// this
key in obj && (x = obj[key], true) && pred(x) ? Just(x) : Nothing
// and this will work
key in obj && (x = obj[key], 1) && pred(x) ? Just(x) : Nothing
but it's a bit cryptic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scroll up for a more elegant solution, @safareli. ;)
eq(S.parseJson(Array, '["foo","bar"]'), S.Just(['foo', 'bar'])); | ||
eq(S.parseJson(S.K(true), '[Invalid JSON]'), S.Nothing); | ||
eq(S.parseJson(S.K(false), '{"foo":"bar"}'), S.Nothing); | ||
eq(S.parseJson(S.K(true), '["foo","bar"]'), S.Just(['foo', 'bar'])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚡
eq(S.parseJson(Array, '["foo","bar"]'), S.Just(['foo', 'bar'])); | ||
eq(S.parseJson(S.is(Object), '[Invalid JSON]'), S.Nothing); | ||
eq(S.parseJson(S.is(Array), '{"foo":"bar"}'), S.Nothing); | ||
eq(S.parseJson(S.is(Array), '["foo","bar"]'), S.Just(['foo', 'bar'])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love to replace the two assertions above with the following:
eq(S.parseJson($.test([], $.Array($.String)), '{"foo":"bar"}'), S.Nothing);
eq(S.parseJson($.test([], $.Array($.String)), '[1,2,3,4,5,6]'), S.Nothing);
eq(S.parseJson($.test([], $.Array($.String)), '["foo","bar"]'), S.Just(['foo', 'bar']));
Very aeson-like!
This requires a recent version of sanctuary-def, though. I'm currently working on the pull request to upgrade our sanctuary-def dependency. :)
var x = obj[key]; | ||
return is(typeRep, x) ? Just(x) : Nothing; | ||
function get(pred, key, obj) { | ||
var x = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on var x = null;
versus var x;
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make it consistent with similar code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized there is no similar case in the entire code base.
//. name, and an object and returns Just the value of the specified object | ||
//. property if it is of the specified type (according to [`is`](#is)); | ||
//. Takes a predicate, a property name, and an object and returns Just the | ||
//. value of the specified object property if it passes the given predicate; | ||
//. Nothing otherwise. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should state that Nothing
is returned if the property does not exist:
Takes a predicate, a property name, and an object and returns Just the value of the specified object property if it passes the property exists and the value satisfies the given predicate; Nothing otherwise.
//. the value is of the specified type; Nothing otherwise. | ||
//. Takes a predicate, an array of property names, and an object and returns | ||
//. Just the value at the path specified by the array of property names if | ||
//. such a path exists and the value passes the given predicate; Nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer "satisfies" in place of "passes".
//. The `Object` type representative may be used as a catch-all since most | ||
//. values have `Object.prototype` in their prototype chains. | ||
//. Takes a predicate, a property name, and an object and returns Just the | ||
//. value of the specified object property if it exists and the value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went for "it" over your suggested "the property".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems slightly ambiguous to me. I see two valid interpretations:
- return Just (the value of the specified object property) if it exists; or
- return Just the value of (the specified object property) if it exists.
"It" can refer to either parenthesized term, depending on where one's brain decides the parens should go. For some reason my brain wants to include "the value of" in the parens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I found that it read more fluently. Referring to the existence of the subject clears the ambiguity to me - it doesn't matter whether we talk about the existence of the property or it's value. Non-existent applies to the whole pair. ;)
I guess you should decide in the end. What will it be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find your argument persuasive. Let's stick with "it". 😀
So what do you think? Merge this before or after #232? |
Go ahead and squash, rebase on |
This commit will close #149 by changing all functions that have built-in type validation to use predicates instead of TypeReps to perform their validations. The functions affected are: * get() * gets() * parseJson() The only function untouched is `is()`, for it is used to turn a TypeRep into a predicate.
18eb8af
to
ddc5f6e
Compare
⚡ |
|
Seems like a test-runner related error. Has to do with running code in child windows like iframes. Shall we retry? |
I re-ran the tests and they worked 😕 |
I've hit that error once too. It seems to be nondeterministic. |
🌳 |
Commit description: