Skip to content
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

Closed
wants to merge 20 commits into from
Closed

Type refinements #162

wants to merge 20 commits into from

Conversation

Avaq
Copy link
Member

@Avaq Avaq commented Jun 26, 2017

Refinement support:

  • NullaryType
  • UnaryType
  • BinaryType
  • RecordType
  • EnumType

Helpers:

  • Strict
  • NonEmpty

Fixes #128
Fixes #141
Contributes to sanctuary-js/sanctuary#384

@Avaq Avaq force-pushed the avaq/type-refinements branch 2 times, most recently from 02f1592 to 1a34c34 Compare June 26, 2017 14:25
index.js Outdated
function NullaryTypeWithUrl(name, test) {
return NullaryType(name, functionUrl(name), test);
// NullaryTypeWithUrl :: (Type, String, Any -> Boolean) -> Type
function NullaryTypeWithUrl(parent, name, test) {
Copy link
Member

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 and test, adjacent; and
  • having name first makes the call sites read better, in my view.

index.js Outdated
types: {},
toString: K('Any')
};
Any.parent = Any;
Copy link
Member

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));
Copy link
Member

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.

Copy link
Member Author

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).

Copy link
Member

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
Copy link
Member

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. :)

Copy link
Member Author

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);
Copy link
Member

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. ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But my line count! :(

Copy link
Member

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); }
);
Copy link
Member

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'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please wrap the closing );.

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,
Copy link
Member

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
Copy link
Member

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! 👏

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the second time ;)

@Avaq Avaq force-pushed the avaq/type-refinements branch 2 times, most recently from e23e6e1 to d2e90df Compare June 26, 2017 14:55
@Avaq
Copy link
Member Author

Avaq commented Jun 26, 2017

let's decide upon some other sorting rule

We could sort types by their level in the type tree, alphabetically. So:

  1. List of types that have Any as a parent, in alphabetical order.
  2. List of types that refine any of the types from 1, in alphabetical order.
  3. List of types that refine any of the types from 2, in alphabetical order.
  4. ...

@Avaq
Copy link
Member Author

Avaq commented Jun 26, 2017

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:

Any
  Boolean
  Function
    Predicate
  Number
    FiniteNumber
    Infinity
...

@davidchambers
Copy link
Member

I like the idea of grouping related types in the readme. It would be nice to have:

  • Number
  • ValidNumber
  • NonZeroValidNumber
  • FiniteNumber
  • NonZeroFiniteNumber

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
@Avaq
Copy link
Member Author

Avaq commented Jun 27, 2017

What do you think of this new order:

Any
  AnyFunction
  Arguments
  Array
  Boolean
  Date
    ValidDate
  Error
  Inconsistent
  Null
  Number
    ValidNumber
      FiniteNumber
        NegativeFiniteNumber
        NonZeroFiniteNumber
        PositiveFiniteNumber
      Integer
        NegativeInteger
        NonZeroInteger
        PositiveInteger
      NegativeNumber
      NonZeroValidNumber
      PositiveNumber
  Object
  RegexFlags
  RegExp
    GlobalRegExp
    NonGlobalRegExp
  String
  Symbol
  Type
  TypeClass
  Undefined
  Unknown
Function
NonEmpty
Nullable
Pair
StrMap

//. tuples. `['foo', 42]` is a member of `Pair String Number`.
var Pair = BinaryTypeWithUrl(
'sanctuary-def/Pair',
Array_(Any),
Copy link
Member Author

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.

Copy link
Member

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. 👍

@Avaq
Copy link
Member Author

Avaq commented Jun 29, 2017

I think we should hold off implementing this for record types, I think it would be better to leave record types refine Object. I think "extending" record types the way you suggested in #128 should be done in a different way. It could have the same API, but not be implemented on top of refinements.

With that out of the way, I think this PR is now "ready".

@Avaq Avaq changed the title WIP: Type refinements Type refinements Jun 29, 2017
@davidchambers
Copy link
Member

I think "extending" record types the way you suggested in #128 should be done in a different way.

Why do you think so?

// Inconsistent :: Type
var Inconsistent =
new _Type(INCONSISTENT, '', '', always2('???'), K(false), [], {});

Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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' +
Copy link
Member

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});
Copy link
Member

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.

Copy link
Member Author

@Avaq Avaq left a 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
Copy link
Member Author

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?

Copy link
Member

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
//. );
Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member Author

@Avaq Avaq Jul 5, 2017

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);
}

Copy link
Member Author

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.

Copy link
Member Author

@Avaq Avaq Jul 5, 2017

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'))

Copy link
Member

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]
);

Copy link
Member Author

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 tested 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.

Copy link
Member

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.

Copy link
Member Author

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_,
Copy link
Member

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.

Copy link
Member Author

@Avaq Avaq Jul 5, 2017

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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/support/supports/

@@ -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() {

Copy link
Member

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);
Copy link
Member

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);

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.
Copy link
Member

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;
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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']

Copy link
Member Author

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.
Copy link
Member

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.

Copy link
Member Author

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);
Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

@Avaq Avaq Jul 10, 2017

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!

Copy link
Member Author

@Avaq Avaq Jul 10, 2017

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

Copy link
Member Author

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.

@davidchambers
Copy link
Member

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. :)

@Avaq
Copy link
Member Author

Avaq commented Dec 8, 2017

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:

I'm having trouble again with the Strict/NonEmpty case. It seemed to have a clear solution last time:

We created type refinements with the purpose of defining strict record types in terms of them, therefore it goes without saying that Strict should be a helper function that takes away the boilerplate of doing so.

We then moved on to conclude that NonEmpty is very similar to Strict, and should probably follow the same logic. However, after defining NonEmpty this way, I've run into an issue that could not be encountered with Strict, to do with t.name. Look at this definition:

 function NonEmpty(t) {
   return NullaryType('sanctuary-def/NonEmpty',
                      functionUrl('NonEmpty'),
                      t,
                      function test(x) {
                        return Z.Monoid.test(x) &&
                               Z.Setoid.test(x) &&
                               !Z.equals(x, Z.empty(x.constructor));
                      });
 }

This will cause the following:

const head = def('head', {}, [$NonEmpty($Array($a)), $a], xs => xs[0]);
head.toString()
-head :: NonEmpty (Array a) -> a
+head :: NonEmpty -> a

We have not run into this issue with Strict, simply because we don't render the name of record types, and have not tested against it. However, I suspect that after making a record type "Strict", it will no longer be rendered very informatively in error messages. All of this leads me to conclude that:

  1. Using a UnaryType for NonEmpty makes sense, because we keep all type information for the inner type.
  2. Using a NullaryType refinement for Strict is not good enough. Especially when we ever add names to record types.

I'm going to explore using a UnaryType for both NonEmpty and Strict, and see if I can still get them to be a refinement as well as a "container" of the input type.


I've modified Type#validate to include a expType :: Type field in the returned record, for example in $.Integer.validate('hello'), this would have a value of $.Number, because that's the actual type for which our validation failed. The propPath then always applies to the expType, and we can even deduce that:

1) "hello" :: String

The value at position 1 is not a member of Integer, because it is not a Number.

...for example. But there are a lot of changes to be made to the code which builds on Type#validate before we are at that point.

I hope you're able to find a way to make $.Strict a unary type.


Type refinements worked so well on the surface, but upon taking a closer look I find that many things are broken under the current approach. Take this for example:

const Point = $.RecordType($.Any, {x: $.Number, y: $.Number});
const Point3D = $.RecordType(Point, {z: $.Number});

const StrictPoint3D = $.Strict(Point3D);

We haven't considered that StrictPoint3D will have 0 members. The Strict wrapper expects an object with one key, because Point3D.keys.length === 1, and when an object with one key is passed, it's invalidated by the parent, which expects two keys.

It's a similar problem to what we faced before. The meta-data on Point3D does not reflect the "full" meta-data of the combined Point+Point3D.

It seems we need to walk the tree to construct the union ourselves. We'll also need to decide what to do if we encounter a contradiction (e.g. {foo: $.Number} and {foo: $.String}).


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:

  1. I called env: remove refinement types from default environment sanctuary#464 in July!

    I've been thinking about the purpose of env, and there is one burning question that arises: Why does sanctuary add types like $.FiniteNumber to the environment? [...] We're adding types to the environment for the sole purpose of showing nicer error messages. For the purpose of type variables, the only types that need to be in the environment are direct descendants of Any. [This] means we can optimize the amount of checks we're doing by ignoring any types where t.parent !== Any.

    And when it comes to reporting all known types of a value, why do we limit ourselves the the environment? It starts to become attractive to simply add all types to the environment. The more types Sanctuary knows about, the more precisely an error message will report what the types of a value are at no extra run-time cost[, because of the previous idea].

  2. This change to the toMaybe API to work more like encase. You asked me to open an issue and I forgot, so here it is: Change toMaybe API to work more like encase sanctuary#465

@Avaq Avaq mentioned this pull request Mar 27, 2019
@davidchambers
Copy link
Member

Shall we close this, Aldwin?

@Avaq
Copy link
Member Author

Avaq commented Apr 6, 2019

Let's do it. The effort is not lost, because it has gotten me familiar with the sanctuary-def code. :)

@Avaq Avaq closed this Apr 6, 2019
@davidchambers davidchambers deleted the avaq/type-refinements branch April 6, 2019 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants