-
-
Notifications
You must be signed in to change notification settings - Fork 24
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
Type refinements #162
Type refinements #162
Conversation
02f1592
to
1a34c34
Compare
index.js
Outdated
function NullaryTypeWithUrl(name, test) { | ||
return NullaryType(name, functionUrl(name), test); | ||
// NullaryTypeWithUrl :: (Type, String, Any -> Boolean) -> Type | ||
function NullaryTypeWithUrl(parent, name, test) { |
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 (name, parent, test)
for two reasons:
- it makes the two constituents of the type's
test
method,parent
andtest
, adjacent; and - having
name
first makes the call sites read better, in my view.
index.js
Outdated
types: {}, | ||
toString: K('Any') | ||
}; | ||
Any.parent = Any; |
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 could still leverage NullaryTypeWithUrl
here, I believe:
var Any = NullaryTypeWithUrl(null, 'sanctuary-def/Any', null);
Any.parent = Any;
Any.test = K(true);
Any.validate = Right;
index.js
Outdated
//# None :: Type | ||
//. | ||
//. Type with no members. | ||
var None = NullaryTypeWithUrl(Any, 'sanctuary-def/None', K(false)); |
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.
Shall we name this Void
to match Haskell? None
could be confusing due to its use as a data constructor (of type Option a
) in Scala.
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 sure if we need this. I added it to use in INCONSISTENT only, but we could as well use Any
in there since the _test
is K(false)
.
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.
Ah, I see! I like the idea of using Any
. :)
index.js
Outdated
'sanctuary-def/FiniteNumber', | ||
function(x) { return ValidNumber._test(x) && isFinite(x); } | ||
isFinite |
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 change is particularly satisfying. :)
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.
Agreed :)
index.js
Outdated
@@ -532,28 +571,35 @@ | |||
$types[k] = {extractor: K([]), type: t}; | |||
}); | |||
|
|||
return new _Type(FUNCTION, '', '', format, test, $keys, $types); | |||
return new _Type(FUNCTION, '', '', format, | |||
AnyFunction, K(true), $keys, $types); |
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 it's not possible to have all the arguments on one line, please start each one at the same column. ;)
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.
But my line count! :(
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 formatting also satisfies my rule:
return new _Type(
FUNCTION, '', '', format, AnyFunction, K(true), $keys, $types
);
You're welcome to use this. :)
index.js
Outdated
Number_, | ||
'sanctuary-def/ValidNumber', | ||
function(x) { return !isNaN(x); } | ||
); |
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.
Since it's no longer possible to list the types in alphabetical order, let's decide upon some other sorting rule.
index.js
Outdated
var TypeClass = NullaryTypeWithUrl( | ||
Any, | ||
'TypeClass', | ||
typeEq('sanctuary-type-classes/TypeClass')); |
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.
Please wrap the closing );
.
1a34c34
to
f0e8340
Compare
index.js
Outdated
//. Using types as predicates is useful in other contexts too. One could, | ||
//. for example, define a [record type][] for each endpoint of a REST API | ||
//. and validate the bodies of incoming POST requests against these types. | ||
//. Using types as predicates is can be useful. One could, for example, |
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.
s/ is//
I think powerful better captures my thought. How about the following?
Using types as predicates is powerful.
index.js
Outdated
@@ -1290,12 +1321,14 @@ | |||
//. | |||
//. sanctuary-def provides several functions for defining types. | |||
|
|||
//# NullaryType :: String -> String -> (Any -> Boolean) -> Type | |||
//# NullaryType :: Type -> String -> String -> (Any -> Boolean) -> Type |
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.
Thank you for remembering to update the documentation! 👏
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 the second time ;)
e23e6e1
to
d2e90df
Compare
d2e90df
to
50e9164
Compare
We could sort types by their level in the type tree, alphabetically. So:
|
I prefer grouping "related" types, so perhaps grouping by depth in the type tree is better, we would sort types alphabetically, but ensure that a types children are always listed directly below their parents, that way Number types will be grouped together. It's a slightly more complex rule, but I think will lead to a more readable documentation, eg:
|
I like the idea of grouping related types in the readme. It would be nice to have:
I'm not sure how to codify the internal rule which makes this ordering feel natural to me. |
The sheme has been discussed in #162
What do you think of this new order:
|
769fd41
to
c5f1130
Compare
//. tuples. `['foo', 42]` is a member of `Pair String Number`. | ||
var Pair = BinaryTypeWithUrl( | ||
'sanctuary-def/Pair', | ||
Array_(Any), |
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 whether to use Array Any
or Array Unknown
here. I think using Any
is fine.
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.
Array Any
is clearer to me. 👍
I think we should hold off implementing this for record types, I think it would be better to leave record types refine With that out of the way, I think this PR is now "ready". |
Why do you think so? |
// Inconsistent :: Type | ||
var Inconsistent = | ||
new _Type(INCONSISTENT, '', '', always2('???'), K(false), [], {}); | ||
|
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's the thinking behind moving this definition? Did it feel inconsistent having it apart from the others?
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.
It depends on Any
being 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.
👍
'create :: { checkTypes :: Boolean, env :: Array Any } -> Function\n' + | ||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + | ||
'create :: { checkTypes :: Boolean, env :: Array Type } -> Function\n' + | ||
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + |
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 a nice improvement. :)
|
||
// Point :: Type | ||
var Point = $.RecordType({x: $.Number, y: $.Number}); | ||
var Point = $.RecordType($.Any, {x: $.Number, y: $.Number}); |
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.
Every record type defined in the test suite currently extends $.Any
. It would be good to include your Point3D
example here as well.
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.
My thoughts regarding the unaddressed feedback.
index.js
Outdated
//. | ||
//. The `p2` value is a member of `Point3D` as well as `Point`, making it a | ||
//. valid argument to `dist`. By default record types allow additional | ||
//. properties (which is what allowed `p2` to be a member of `Point`). If one |
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 want readers to be aware that permitting additional properties is a prerequisite to allowing record extension as described in the prior section. Maybe this more concrete clause is more to your liking?
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 ❤️ the concrete clause. In fact, I would lead with it:
By default, record types permit the presence of additional fields. As a result, p2
is a member of Point
as well as Point3D
.
One could define Strict :: Type -> Type
and use it to define record types which do not permit additional fields:
index.js
Outdated
//. '', | ||
//. t, | ||
//. x => Object.keys(x).length === t.keys.length | ||
//. ); |
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.
How do you propose this be made into a UnaryType? I see one hacky way:
// Strict :: Type -> Type
const Strict = t => $.UnaryType(
'Strict' + t.name,
t.url,
t,
x => Object.keys(x).length === t.keys.length,
x => [x]
)(t); //<- That's unfortunate
The "proper" way leads to a dead end:
// Strict :: Type -> Type
const Strict = $.UnaryType(
'my-package/Strict', //<- That's unfortunate
'http://example.com/#strict', //<- That's unfortunate
$.Any, //<- That's unfortunate
x => Object.keys(x).length === `???`, //<- That's impossible
x => [x]
);
I currently think strict
makes more sense as a "common refinement" (which justifies its inclusion in the library), and I probably would have also made nonEmpty
in that way.
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 advantage of having NonEmpty be a UnaryType is that "abc"
is automatically a member of it, correct? So we don't have to explicitly create NonEmptyString
and add it to the environment.
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.
Maybe the test
could be something along the lines of:
x => {
const len = Object.keys(x).length;
return determineActualTypesStrict(x).every(type => type.keys.length === len);
}
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 first "hacky" approach will not have the same benefits as NonEmpty
. We have to find an approach where Strict(Unknown)
has members.
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.
Altough, I don't have to add my Strict type to the environment anyway, I can create it at run time without drawback, I think:
strict(User).validate(userInput)
const createUser = def('createUser',
{}, [strict($User), $Future($Error, $UserId)],
Future.encase2(mongo.insertOne, 'users'))
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.
How about this approach?
// Strict :: Type -> Type
const Strict = $.UnaryType(
'my-package/Strict',
'http://example.com/my-package#Strict',
$.Any,
function(x) {
const t = this.types.$1.type;
return t.type === 'RECORD' &&
$.test([], t, x) &&
Object.keys(x).length === t.keys.length;
},
strict => [strict]
);
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.
Woah. Using this
inside a test
function is new, correct? There are some places where we're passing t._test
down into a new Type, I'm not sure if the consequences of relying on this
will include breakage of code relying on the "purity" (this
-lessness?) of _test
. If it doesn't, then the fact that we can use this
to refer to the type of a value being test
ed is actually quite neat.
I do still doubt whether using UnaryType
for this purpose is useful though. When implementing this as a type refinement, we get the elegance of strict(User)
being a refinement of User
. When implementing this as a UnaryType
, I'm not sure if we gain anything; I believe that adding Strict(Unknown)
to the environment will not cause {name: "Bob"} :: User, Strict User
, because Unknown.type !== 'RECORD'
. This differs from NonEmpty
, as its check does not rely on the type of types.$1
.
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.
Referencing this
does feel naughty, I agree. Let's stick with the definition you have, but capitalize the identifier in accordance with our convention of capitalizing the names of types and type constructors.
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 have a much stronger argument:
> var User = $.RecordType($.Any, {name: $.NonEmpty($.String)})
> User.validate({name: ''})
Left({ value: '', propPath: [ 'name' ] })
> strict(User).validate({name: ''});
Left({ value: '', propPath: [ 'name' ] })
> Strict(User)
Left({ value: { name: '' }, propPath: [] })
Or in words: Your UnaryType
relies on $.test
, and the purpose of parent types was to fix validate
for those types that relied on test
-ing another type. Actually, the purpose of this PR was to fix #128, and in particular, to address my comment there: #128 (comment)
index.js
Outdated
@@ -2538,6 +2668,7 @@ | |||
PositiveNumber: PositiveNumber, | |||
RegExp: RegExp_, | |||
RegexFlags: RegexFlags, | |||
Strict: Strict, | |||
StrMap: fromUncheckedUnaryType(StrMap), | |||
String: String_, |
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've been using Array#sort
to determine order:
> ['Strict', 'StrMap', 'String'].sort()
['StrMap', 'Strict', 'String']
Please also move the definition of Strict
.
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 tried, but found that my editors built-in sorting mechanism changed the position of String. I'll switch to use Array#sort
. :)
test/index.js
Outdated
@@ -1345,6 +1357,22 @@ describe('def', function() { | |||
'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n'); | |||
}); | |||
|
|||
it('support "strict" record types', function() { |
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.
s/support/supports/
4ec5ba4
to
9708db1
Compare
@@ -1345,6 +1357,22 @@ describe('def', function() { | |||
'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n'); | |||
}); | |||
|
|||
it('supports "strict" record types', function() { | |||
|
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.
For consistency, please remove this empty line and the one before the closing });
.
test/index.js
Outdated
var p = {x: 1, y: 2, z: 3}; | ||
|
||
eq($.test([], Point, p), true); | ||
eq($.test([], StrictPoint, p), false); |
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 like to see four assertions here:
eq($.test([], Point, {x: 1, y: 2}), true);
eq($.test([], Point, {x: 1, y: 2, z: 3}), true);
eq($.test([], StrictPoint, {x: 1, y: 2}), true);
eq($.test([], StrictPoint, {x: 1, y: 2, z: 3}), false);
94ed207
to
cc92cd2
Compare
index.js
Outdated
//. Constructor for strict record types. For example: | ||
//. `$.Strict($.Record($.Any, {name: $.String}))`, is the type comprising | ||
//. every object with a `name :: String` property, without any other | ||
//. 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.
Do you like this variant?
Constructor for strict record types.
$.Strict($.Record($.Any, {name: $.String}))
, for example, is the type comprising every object with exactly one field, name
, of type String
.
index.js
Outdated
//. properties. | ||
function Strict(t) { | ||
function test(x) { | ||
return Object.keys(x).length === t.keys.length; |
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.
Object.keys
ignores inherited properties. Is this desirable?
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 thought about that, and I don't think we can make a sensible Strict constructor 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.
Is using for...in
impractical for some reason?
function O() {}
var o = new O();
o.direct = true;
O.prototype.indirect = true;
Object.keys(o);
// => ['direct']
(function() { var keys = []; for (var k in o) keys.push(k); return keys; }());
// => ['direct', 'indirect']
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 use keys.every(k => k in o)
as a test
for record types. Will this lead to problems?
> 'then' in new Promise(() => {})
true
> (function(o) { var keys = []; for (var k in o) keys.push(k); return keys; }(new Promise(() => {})))
[]
This would mean that:
const Thenable = $.RecordType({then: $.AnyFunction});
const StrictThenable = $.Strict(Thenable);
Thenable.validate(new Promise(noop)) // => Right
StrictThenable.validate(new Promise(noop)) // => Left
I'm not sure if that's desired. Using enumerable keys over "own" keys is an improvement though, so I think I should implement your suggestion nonetheless.
index.js
Outdated
//. The `p2` value is a member of `Point3D` as well as `Point`, making it a | ||
//. valid argument to `dist`. By default, record types permit the presence | ||
//. of additional fields. As a result, p2 is a member of Point as well as | ||
//. Point3D. |
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.
p2
, Point
, and Point3D
should be wrapped in backticks.
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.
Darned copy-paste ;)
index.js
Outdated
); | ||
} | ||
function extract(monoid) { return [monoid]; } | ||
var t = UnaryTypeWithUrl('sanctuary-def/NonEmpty', parent, test, extract); |
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.
Using parent
as an actual parent type, like I'm doing here, seems like a good idea. Now NonEmpty String
will (correctly) be a "refinement" of String
.
However, as indicated by the failing unit test, it causes a value of [1]
for $NonEmpty($Array($String))
, for example, to generate this error message:
NonEmpty (Array String)
^^^^^^^^^^^^^^
1
1) 1 :: Number
The value at position 1 is not a member of ‘Array String’.
Where we would want:
NonEmpty (Array String)
^^^^^^
1
1) 1 :: Number
The value at position 1 is not a member of ‘String’.
I'm not sure why this happens. Replacing parent
with Any
solves the issue.
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 problem is that
$.NonEmpty($.Array($.String)).validate([1, 2, 3]).value.propPath
evaluates to ['$1']
rather than ['$1', '$1']
because we first validate the parent type and, if validation fails, make no adjustment to propPath
to account for the extra level of nesting introduced by $.NonEmpty
.
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.
That seems like an issue that will bite us in some other way, one day. I don't yet see a solution.
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.
In it's general form, I think the issue could be described as: When the validation of a refined type fails on its parent type, the reported propPath
may not correctly correspond to the location of the value as it exists in the refined type
We even encounter this issue if we refine a record type to be "strict", as has always been the suggestion:
const User = $.RecordType({ name: $.String, password: $.String });
const StrictUser = $.NullaryType('', '', User, x => Object.keys(x).length ===
User.keys.length);
const validation = StrictUser.validate({ name: 'bob', password: 1 });
// propPath = [ 'password' ]
StrictUser.types[validation.value.propPath[0]] // => undefined
// the above expression should never return undefined, as the propPath must
// correspond to the types mapping. It does because our propPath is actually
// corresponding to another types mapping!
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 same problem, biting us somewhere else:
const Point = $.RecordType($.Any, { x: $.Number, y: $.Number });
const Point3D = $.RecordType(Point, { z: $.Number });
const validation = Point3D.validate({ x: 1, y: 'two', z: 3 });
// propPath = [ 'y' ]
Point3D.types[validation.value.propPath[0]] // => 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 is particularly problematic, because the property names described by propPath
might exist in both the parent type and the refinement, and validate
does not tell us which it was.
Do you remember where we left this, Aldwin? In light of sanctuary-js/sanctuary#464 I'm very excited by the possibility of merging this pull request. :) |
Hi David. I don't think this will be ready to merge anytime soon. We left it at the point where we realised that this brings more problems than it solves. Here's some exempts from our Gitter conversation at the time:
That's the last few thoughts we had on the subject, and where things came to an unfortunate halt. Incidentally, our Gitter conversation is a treasure trove of ideas! Here's some:
|
Shall we close this, Aldwin? |
Let's do it. The effort is not lost, because it has gotten me familiar with the sanctuary-def code. :) |
Refinement support:
Helpers:
Fixes #128
Fixes #141
Contributes to sanctuary-js/sanctuary#384