Repository: plaid/sanctuary
Pull request #64 (2015-07-17 18:31:41)

add type argument to S.get, S.gets, and S.pluck
The fact that `S.get` returns a value of type `Maybe *` has bothered me for some time. I find myself filtering the result to ensure I have a value of the expected type. For example:

```javascript
R.pipe(...,
       S.get('x'),
       R.filter(R.is(Number)),
       ...)
```

This pull request adds a type specifier to `S.get`, `S.gets`, and `S.pluck`. The above will become:

```javascript
R.pipe(...,
       S.get(Number, 'x'),
       ...)
```

The current loose behaviour can be approximated by `S.get(Object)`. It's no longer possible to get null/undefined values. If this is important we could export an Any pseudotype, where Any contains every JavaScript value. Unless there's a use case, I'd prefer not to export and document `S.Any`.

These changes build upon the foundation laid by #63. I've included a section explaining the Accessible pseudotype, as it's now used in type signatures.

One question I have is how to link `Type` and `a` in a signature such as:

```haskell
get :: Type -> String -> Accessible -> Maybe a
```

Any suggestions?


pvrnav: This should be really useful @davidchambers! 

I think saying `Typeof a` is very readable, but there's possibly a better convention out there somewhere.
benperez: > One question I have is how to link Type and a in a signature

You could use the syntax for a class constraint.
```haskell
get :: Type a => Type -> String -> Accessible -> Maybe a
```

davidchambers: > You could use the syntax for a class constraint.
>
> ```haskell
> get :: Type a => Type -> String -> Accessible -> Maybe a
> ```

Since `a` is already a type variable, I'm not sure what information the `Type a` constraint adds. Also, I don't see how this relates the return type to the type of the first argument.

joneshf: I think you have to be the type checker in this case. Look at where `Type` flows through your program, and make the decision based on that.

It is sent to `is` in [`get`](https://github.com/plaid/sanctuary/pull/64/files#diff-168726dbe96b3ce427e7fedce31bb0bcR1299)

In [`is`](https://github.com/plaid/sanctuary/pull/64/files#diff-168726dbe96b3ce427e7fedce31bb0bcR120) it is compared to [`Accessible`](https://github.com/plaid/sanctuary/pull/64/files#diff-168726dbe96b3ce427e7fedce31bb0bcR112). So it should probably have a type like: `() -> Accessible`.

It also is used as the RHS of `instanceof` on the same line. So, it must be a constructor. Since what it is checked against is a value (the `a`) it should have a type like: `() -> a`.

Combine the two types and you end up with something like: `() -> (Accessible | a)`, where `|` represents a union.

So putting it all together you get something like: 

```haskell
get :: (() -> (Accessible | a)) -> String -> Accessible -> Maybe a
```
davidchambers: @joneshf, I don't understand how you arrived at `()` in `() -> (Accessible | a)`. Are you using `()` to indicate zero arguments? Take `String` as an example of a type:

```javascript
> String(42)
'42'
> String(null)
'null'
```

This suggests we have something like `String :: * -> String`. But the "constructor" can be given no arguments (in which case it returns `''`). Then we have `RegExp` which accepts multiple arguments. So perhaps we should replace `Type` with `*... -> a`. But there are an infinite number of functions which return a value of type `a` for any given `a`, not just the `a` constructor.

How about this?

```haskell
get :: A -> String -> Accessible -> Maybe a
```

This would mean we're treating single-letter identifiers—rather than lower-case identifiers—as type variables. This is a deviation from Haskell, but the correspondence between `A` and `a` is quite nice.

tel: One thing that might work is

    TypeRep a -> String -> Accessible -> Maybe a

This is more exact since the values like `Number` are value-level representatives of types, not types themselves (this is confusing nomenclature in Javascript, since the word "type" is so overloaded, but it is certainly accurate).

We can imagine an indexed family of types of kind `TypeRep : * -> *` where for each type `a`, `TypeRep a` has exactly one value—a singleton. This value represents the types at the value level.
davidchambers: @tel, are you suggesting that `Number` is the sole value of the `TypeRep Number` type? I want to make sure I've understood your comment. :)

tel: Yeah, exactly! We cannot instantiate this type directly in Haskell, but it'd look a bit like this

```haskell
data TypeRep a where
  Number :: TypeRep Number
  Array :: TypeRep a -> TypeRep (Array a)
  ...
```

The difficulty is that in Haskell we need to ultimately close the type (end it after finitely many variants have been specified) yet we're allowed to continue specifying new types afterward. Worse, there's no way to get values at `TypeRep (TypeRep X)`, e.g.

But ignoring those issues that's the exact idea.
davidchambers: Okay. I like the idea. Do these changes seem right?

```diff
- is :: Type -> a -> Boolean
+ is :: Type a -> b -> Boolean

- Maybe :: Type
+ Maybe :: Type Maybe

- Maybe#type :: Type
+ Maybe#type :: Type Maybe

- Either :: Type
+ Either :: Type Either

- Either#type :: Type
+ Either#type :: Type Either

- pluck :: String -> [{String: *}] -> [Maybe *]
+ pluck :: Type a -> String -> [Accessible] -> [Maybe a]

- get :: String -> Object -> Maybe *
+ get :: Type a -> String -> Accessible -> Maybe a

- gets :: [String] -> Object -> Maybe *
+ gets :: Type a -> [String] -> Accessible -> Maybe a
```

Unless there's a compelling reason to use `TypeRef` rather than `Type`, I'd prefer to stick with the latter since we're already using it (`S.Just(42).type`, for example, evaluates to `S.Maybe`).

tel: I suppose those work, but using `Type` directly feels very confusing to me. `Maybe :: Type Maybe` at the value level certainly isn't a type after all! Generally that's looking good though.

davidchambers: Yeah, `TypeRef` *is* much better. I'll update the type signatures but leave the `type` property alone for now. We can consider renaming it `typeref` or `typeRef` in a separate pull request.

davidchambers: I've changed occurrences of `Type` in type signatures to `TypeRep` (not `TypeRef` as I mistakenly wrote in earlier comments).

Setting aside type signatures for a moment, do others agree with the API change in question? Do you find the rationale compelling, @svozza?

svozza: I definitely like this, the main issues I used to have with `gets` was that often there'd be a chance that they'd just be wrapping `null`s, which meant I actually avoided using it. Big +1 from me.

Patch:
diff --git a/index.js b/index.js
index 64d6b0b..fb5f400 100644
--- a/index.js
+++ b/index.js
@@ -59,6 +59,30 @@
 //. (for any type `a`) with an argument of type `a -> b` (for any type `b`),
 //. it returns a value of type `Maybe b`._
 //.
+//. ### Accessible pseudotype
+//.
+//. What is the type of values which support property access? In other words,
+//. what is the type of which every value except `null` and `undefined` is a
+//. member? Object is close, but `Object.create(null)` produces a value which
+//. supports property access but which is not a member of the Object type.
+//.
+//. Sanctuary uses the Accessible pseudotype to represent the set of values
+//. which support property access.
+//.
+//. ### Type representatives
+//.
+//. What is the type of `Number`? One answer is `a -> Number`, since it's a
+//. function which takes an argument of any type and returns a Number value.
+//. When provided as the first argument to [`is`](#is), though, `Number` is
+//. really the value-level representative of the Number type.
+//.
+//. Sanctuary uses the TypeRep pseudotype to describe type representatives.
+//. For example:
+//.
+//.     Number :: TypeRep Number
+//.
+//. `Number` is the sole inhabitant of the TypeRep Number type.
+//.
 //. ## API
 
 ;(function() {
@@ -101,13 +125,13 @@
   });
 
   var Accessible = /* istanbul ignore next */ function Accessible() {};
-  var Type = /* istanbul ignore next */ function Type() {};
+  var TypeRep = /* istanbul ignore next */ function TypeRep() {};
   var a = {name: 'a'};
   var b = {name: 'b'};
   var c = {name: 'c'};
 
   var _is = function(type, x) {
-    return x != null && Object(x) instanceof type;
+    return x != null && (type === Accessible || Object(x) instanceof type);
   };
 
   var arity = function(n, f) {
@@ -119,7 +143,7 @@
     }
   };
 
-  //  curry :: (String, [Type], [*], Function) -> Function
+  //  curry :: (String, [TypeRep], [*], Function) -> Function
   var curry = function(name, types, _values, f) {
     return arity(R.filter(placeholder, _values).length, function() {
       var values = _values;  // Locally scoped variable to update.
@@ -161,7 +185,7 @@
               [paramIndex, name]
             ));
           }
-        } else if (!_is(type === Type ? Function : type, arg)) {
+        } else if (!_is(type === TypeRep ? Function : type, arg)) {
           throw new TypeError(format(
             '{quote} requires a value of type {type} as its {ord} argument; ' +
             'received {repr}',
@@ -210,11 +234,11 @@
 
   //. ### Classify
 
-  //# is :: Type -> a -> Boolean
+  //# is :: TypeRep a -> b -> Boolean
   //.
-  //. Takes a type and a value of any type and returns `true` if the given
-  //. value is of the specified type (either directly or via the prototype
-  //. chain); `false` otherwise.
+  //. Takes a [type representative](#type-representatives) and a value of
+  //. any type and returns `true` if the given value is of the specified
+  //. type (either directly or via the prototype chain); `false` otherwise.
   //.
   //. Boolean, number, string, and symbol [primitives][] are promoted to
   //. their object equivalents. `42`, for example, is considered a Number
@@ -231,7 +255,7 @@
   //. > S.is(String, 42)
   //. false
   //. ```
-  var is = S.is = def('is', [Type, a], _is);
+  var is = S.is = def('is', [TypeRep, a], _is);
 
   //. ### Combinator
 
@@ -253,7 +277,7 @@
 
   //. ### Maybe type
 
-  //# Maybe :: Type
+  //# Maybe :: TypeRep Maybe
   //.
   //. The Maybe type represents optional values: a value of type `Maybe a` is
   //. either a Just whose value is of type `a` or a Nothing (with no value).
@@ -480,7 +504,7 @@
   //. "Just([1, 2, 3])"
   //. ```
 
-  //# Maybe#type :: Type
+  //# Maybe#type :: TypeRep Maybe
   //.
   //. A reference to the Maybe type. Useful for determining whether two
   //. values such as `S.Nothing()` and `S.Just(42)` are of the same type.
@@ -648,7 +672,7 @@
 
   //. ### Either type
 
-  //# Either :: Type
+  //# Either :: TypeRep Either
   //.
   //. The Either type represents values with two possibilities: a value of type
   //. `Either a b` is either a Left whose value is of type `a` or a Right whose
@@ -822,7 +846,7 @@
   //. "Right([1, 2, 3])"
   //. ```
 
-  //# Either#type :: Type
+  //# Either#type :: TypeRep Either
   //.
   //. A reference to the Either type. Useful for determining whether two
   //. values such as `S.Left('Cannot divide by zero')` and `S.Right(42)`
@@ -1308,59 +1332,77 @@
   //. ```
   S.lastIndexOf = sanctifyIndexOf('lastIndexOf');
 
-  //# pluck :: String -> [{String: *}] -> [Maybe *]
+  //# pluck :: TypeRep a -> String -> [Accessible] -> [Maybe a]
   //.
-  //. Takes a list of objects and plucks the value of the specified key
-  //. for each object in the list. Returns the value wrapped in a Just
-  //. if an object has the key and a Nothing if it does not.
+  //. Takes a [type representative](#type-representatives), a property name,
+  //. and a list of objects and returns a list of equal length. Each element
+  //. of the output list is Just the value of the specified property of the
+  //. corresponding object if the value is of the specified type (according
+  //. to [`is`](#is)); Nothing otherwise.
   //.
-  //. ```javascript
-  //. > S.pluck('a', [{a: 1, b: 2}, {a: 4, b: 5}, {b: 3, c: 7}])
-  //. [Just(1), Just(4), Nothing()]
+  //. See also [`get`](#get).
   //.
-  //. > S.pluck('x', [{x: 1}, {x: 2}, {x: undefined}])
-  //. [Just(1), Just(2), Just(undefined)]
+  //. ```javascript
+  //. > S.pluck(Number, 'x', [{x: 1}, {x: 2}, {x: '3'}, {x: null}, {}])
+  //. [Just(1), Just(2), Nothing(), Nothing(), Nothing()]
   //. ```
-  S.pluck = def('pluck', [String, Accessible], function(key, xs) {
-    return R.map(get(key), xs);
+  S.pluck =
+  def('pluck', [TypeRep, String, Accessible], function(type, key, xs) {
+    return R.map(get(type, key), xs);
   });
 
   //. ### Object
 
-  //# get :: String -> Object -> Maybe *
+  //# get :: TypeRep a -> String -> Accessible -> Maybe a
   //.
-  //. Takes a property name and an object and returns Just the value of
-  //. the specified property of the object if the object has such an own
-  //. property; Nothing otherwise.
+  //. Takes a [type representative](#type-representatives), a property
+  //. 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));
+  //. Nothing otherwise.
+  //.
+  //. The `Object` type representative may be used as a catch-all since most
+  //. values have `Object.prototype` in their prototype chains.
+  //.
+  //. See also [`gets`](#gets).
   //.
   //. ```javascript
-  //. > S.get('x', {x: 1, y: 2})
+  //. > S.get(Number, 'x', {x: 1, y: 2})
   //. Just(1)
   //.
-  //. > S.get('toString', {x: 1, y: 2})
+  //. > S.get(Number, 'x', {x: '1', y: '2'})
+  //. Nothing()
+  //.
+  //. > S.get(Number, 'x', {})
   //. Nothing()
   //. ```
   var get = S.get =
-  def('get', [String, Accessible],
-      R.ifElse(R.has, R.compose(Just, R.prop), Nothing));
+  def('get', [TypeRep, String, Accessible], function(type, key, obj) {
+    return filter(is(type), Just(obj[key]));
+  });
 
-  //# gets :: [String] -> Object -> Maybe *
+  //# gets :: TypeRep a -> [String] -> Accessible -> Maybe a
+  //.
+  //. Takes a [type representative](#type-representatives), a list of property
+  //. names, and an object and returns Just the value at the path specified by
+  //. the list of property names if such a path exists and the value is of the
+  //. specified type; Nothing otherwise.
   //.
-  //. Takes a list of property names and an object and returns Just the
-  //. value at the path specified by the list of property names if such
-  //. a path exists; Nothing otherwise.
+  //. See also [`get`](#get).
   //.
   //. ```javascript
-  //. > S.gets(['a', 'b', 'c'], {a: {b: {c: 42}}})
+  //. > S.gets(Number, ['a', 'b', 'c'], {a: {b: {c: 42}}})
   //. Just(42)
   //.
-  //. > S.gets(['a', 'b', 'c'], {})
+  //. > S.gets(Number, ['a', 'b', 'c'], {a: {b: {c: '42'}}})
+  //. Nothing()
+  //.
+  //. > S.gets(Number, ['a', 'b', 'c'], {})
   //. Nothing()
   //. ```
-  S.gets = def('gets', [Accessible, Accessible], function(keys, obj) {
-    return R.reduce(function(acc, key) {
-      return R.chain(get(key), acc);
-    }, Just(obj), keys);
+  S.gets =
+  def('gets', [TypeRep, Accessible, Accessible], function(type, keys, obj) {
+    var f = function(m, k) { return R.chain(get(Accessible, k), m); };
+    return filter(is(type), R.reduce(f, Just(obj), keys));
   });
 
   //. ### Parse


Test patch:
diff --git a/test/index.js b/test/index.js
index f2a3cad..c3ebef9 100644
--- a/test/index.js
+++ b/test/index.js
@@ -70,7 +70,7 @@ describe('classify', function() {
     it('type checks its arguments', function() {
       assert.throws(function() { S.is([1, 2, 3]); },
                     errorEq(TypeError,
-                            '‘is’ requires a value of type Type ' +
+                            '‘is’ requires a value of type TypeRep ' +
                             'as its first argument; received [1, 2, 3]'));
     });
 
@@ -1940,48 +1940,41 @@ describe('list', function() {
 
   describe('pluck', function() {
 
-    it('is a binary function', function() {
+    it('is a ternary function', function() {
       eq(typeof S.pluck, 'function');
-      eq(S.pluck.length, 2);
+      eq(S.pluck.length, 3);
     });
 
     it('type checks its arguments', function() {
       assert.throws(function() { S.pluck([1, 2, 3]); },
                     errorEq(TypeError,
-                            '‘pluck’ requires a value of type String ' +
+                            '‘pluck’ requires a value of type TypeRep ' +
                             'as its first argument; received [1, 2, 3]'));
 
-      assert.throws(function() { S.pluck('x', null); },
+      assert.throws(function() { S.pluck(Number, [1, 2, 3]); },
                     errorEq(TypeError,
-                            'The second argument to ‘pluck’ ' +
-                            'cannot be null or undefined'));
-    });
-
-    it('returns array of Justs for found keys', function() {
-      var xs = [{a: 1, b: 3}, {a: 2, b: 4}, {a: 3, b: 5}];
-      eq(S.pluck('a', xs), [S.Just(1), S.Just(2), S.Just(3)]);
-    });
-
-    it('returns array of Nothings for keys not found', function() {
-      var xs = [{a: 1, b: 3}, {a: 2, b: 4}, {a: 3, b: 5}];
-      eq(S.pluck('c', xs), [S.Nothing(), S.Nothing(), S.Nothing()]);
-    });
+                            '‘pluck’ requires a value of type String ' +
+                            'as its second argument; received [1, 2, 3]'));
 
-    it('returns Just(undefined) for defined key with no value', function() {
-      var xs = [{a: 1, b: 3}, {a: void 0, b: 4}, {a: 3, b: 5}];
-      eq(S.pluck('a', xs), [S.Just(1), S.Just(undefined), S.Just(3)]);
+      assert.throws(function() { S.pluck(Number, 'x', null); },
+                    errorEq(TypeError,
+                            'The third argument to ‘pluck’ ' +
+                            'cannot be null or undefined'));
     });
 
-    it('returns an array of Maybes for various values', function() {
-      var xs = [{a: 1}, {a: void 0}, {a: 4}, {b: 1}];
-      eq(S.pluck('a', xs),
-         [S.Just(1), S.Just(undefined), S.Just(4), S.Nothing()]);
+    it('returns a list of satisfactory plucked values', function() {
+      var xs = [{x: '1'}, {x: 2}, {x: null}, {x: undefined}, {}];
+      eq(S.pluck(Number, 'x', []), []);
+      eq(S.pluck(Number, 'x', xs),
+         [S.Nothing(), S.Just(2), S.Nothing(), S.Nothing(), S.Nothing()]);
+      eq(S.pluck(Object, 'x', xs),
+         [S.Just('1'), S.Just(2), S.Nothing(), S.Nothing(), S.Nothing()]);
     });
 
     it('is curried', function() {
-      var xs = [{x: 1}, {x: 2}, {x: 3}];
-      eq(S.pluck('x').length, 1);
-      eq(S.pluck('x')(xs), [S.Just(1), S.Just(2), S.Just(3)]);
+      eq(S.pluck(Number).length, 2);
+      eq(S.pluck(Number)('x').length, 1);
+      eq(S.pluck(Number)('x')([{x: 42}]), [S.Just(42)]);
     });
 
   });
@@ -1992,67 +1985,83 @@ describe('object', function() {
 
   describe('get', function() {
 
-    it('is a binary function', function() {
+    it('is a ternary function', function() {
       eq(typeof S.get, 'function');
-      eq(S.get.length, 2);
+      eq(S.get.length, 3);
     });
 
     it('type checks its arguments', function() {
       assert.throws(function() { S.get([1, 2, 3]); },
                     errorEq(TypeError,
-                            '‘get’ requires a value of type String ' +
+                            '‘get’ requires a value of type TypeRep ' +
                             'as its first argument; received [1, 2, 3]'));
 
-      assert.throws(function() { S.get('x', null); },
+      assert.throws(function() { S.get(Number, [1, 2, 3]); },
                     errorEq(TypeError,
-                            'The second argument to ‘get’ ' +
+                            '‘get’ requires a value of type String ' +
+                            'as its second argument; received [1, 2, 3]'));
+
+      assert.throws(function() { S.get(Number, 'x', null); },
+                    errorEq(TypeError,
+                            'The third argument to ‘get’ ' +
                             'cannot be null or undefined'));
     });
 
     it('returns a Maybe', function() {
       var obj = {x: 0, y: 42};
-      eq(S.get('x', obj), S.Just(0));
-      eq(S.get('y', obj), S.Just(42));
-      eq(S.get('z', obj), S.Nothing());
+      eq(S.get(Number, 'x', obj), S.Just(0));
+      eq(S.get(Number, 'y', obj), S.Just(42));
+      eq(S.get(Number, 'z', obj), S.Nothing());
+      eq(S.get(String, 'x', obj), S.Nothing());
+      eq(S.get(Object, 'x', obj), S.Just(0));
     });
 
     it('is curried', function() {
-      eq(S.get('x').length, 1);
-      eq(S.get('x')({x: 42}), S.Just(42));
+      eq(S.get(Number).length, 2);
+      eq(S.get(Number)('x').length, 1);
+      eq(S.get(Number)('x')({x: 42}), S.Just(42));
     });
 
   });
 
   describe('gets', function() {
 
-    it('is a binary function', function() {
+    it('is a ternary function', function() {
       eq(typeof S.gets, 'function');
-      eq(S.gets.length, 2);
+      eq(S.gets.length, 3);
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.gets(null); },
+      assert.throws(function() { S.gets([1, 2, 3]); },
                     errorEq(TypeError,
-                            'The first argument to ‘gets’ ' +
-                            'cannot be null or undefined'));
+                            '‘gets’ requires a value of type TypeRep ' +
+                            'as its first argument; received [1, 2, 3]'));
 
-      assert.throws(function() { S.gets([], null); },
+      assert.throws(function() { S.gets(Number, null); },
                     errorEq(TypeError,
                             'The second argument to ‘gets’ ' +
                             'cannot be null or undefined'));
+
+      assert.throws(function() { S.gets(Number, [], null); },
+                    errorEq(TypeError,
+                            'The third argument to ‘gets’ ' +
+                            'cannot be null or undefined'));
     });
 
     it('returns a Maybe', function() {
       var obj = {x: {z: 0}, y: 42};
-      eq(S.gets([], obj), S.Just({x: {z: 0}, y: 42}));
-      eq(S.gets(['y'], obj), S.Just(42));
-      eq(S.gets(['z'], obj), S.Nothing());
-      eq(S.gets(['x', 'z'], obj), S.Just(0));
+      eq(S.gets(Number, ['x'], obj), S.Nothing());
+      eq(S.gets(Number, ['y'], obj), S.Just(42));
+      eq(S.gets(Number, ['z'], obj), S.Nothing());
+      eq(S.gets(Number, ['x', 'z'], obj), S.Just(0));
+      eq(S.gets(Number, [], obj), S.Nothing());
+      eq(S.gets(Object, [], obj), S.Just({x: {z: 0}, y: 42}));
     });
 
     it('is curried', function() {
-      eq(S.gets(['x']).length, 1);
-      eq(S.gets(['x'])({x: 42}), S.Just(42));
+      eq(S.gets(Number).length, 2);
+      eq(S.gets(Number)(['x']).length, 1);
+      eq(S.gets(Number)(['x'])({x: 42}), S.Just(42));
     });
 
   });