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

Add prop function #161

Merged
merged 1 commit into from
Apr 13, 2016
Merged

Add prop function #161

merged 1 commit into from
Apr 13, 2016

Conversation

svozza
Copy link
Member

@svozza svozza commented Mar 4, 2016

As discussed in #159. I've a few reservations about this, firstly if I define S.prop as var prop = S.prop = def(... then I get lots of errors in the tests when I replace instances of R.prop in index.js:

TypeError: prop is not a function
at createSanctuary (/home/stefano/git/sanctuary/index.js:9:16296)
at /home/stefano/git/sanctuary/index.js:9:45767
at _ (/home/stefano/git/sanctuary/index.js:9:268)
at Object. (/home/stefano/git/sanctuary/index.js:9:779)

Another thing is that Accessible is a very permissive type for this function, it allows you to pass numbers or booleans in even though they don't support property access so you end up getting error messages that leak implementation details to the user:

S.prop('a', true)
// => TypeError: Cannot use 'in' operator to search for 'a' in true

Also, I think it's strange that a function that operates on Records should consider keys that are present in the prototype chain. What are people's thoughts?

@davidchambers
Copy link
Member

I think it's strange that a function that operates on Records should consider keys that are present in the prototype chain.

It's not clear to me which is the most appropriate membership check.

I can, though, provide what I believe is a compelling example in favour of key in obj:

//  Pair :: (a, b) -> Pair a b
function Pair(fst, snd) {
  this[0] = fst;
  this[1] = snd;
}

Pair.prototype.length = 2;

Should S.prop('length', new Pair(0, 0)) evaluate to 2? I argue that it should. Otherwise we'd be drawing a distinction between two equivalent implementations (length as a prototype property or as an instance property).

Also, the usual traps with inherited properties affect object-as-dictionary rather than object-as-record. When using an object to keep track of claimed usernames, name in usernames would erroneously evaluate true for name values such as 'toString' and 'valueOf'. When using an object as a record, though, one asks for a known property. Presumably if one writes S.prop('toString', obj) one wants to reference the toString method of obj (regardless of whether it's attached to obj directly).

return obj[key];
} else {
throw new TypeError('‘prop’ expected object to have a property named ' +
'‘' + key + '’; ' + R.toString(obj) + ' does not');
Copy link
Member

Choose a reason for hiding this comment

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

Let's indent this line to align it with the first ' on the preceding line.

@kedashoe
Copy link
Member

kedashoe commented Mar 4, 2016

So kind of a more type-dangerous get? Might be nice to mention get in the description (and this in the description for get.. do we have @sees?), how they're different, when you might use one vs the other.

Should we include props in this PR?

@davidchambers
Copy link
Member

prop and get are certainly related, but should be used in different contexts. get and gets perform a role similar to that of Haskell's Aeson library: providing a safe way to interact with uncertain data.

We don't use @see (as I detest JSDoc), but we should certainly include reciprocal links and a sentence or two describing the differences.

@kedashoe
Copy link
Member

kedashoe commented Mar 4, 2016

prop and get are certainly related, but should be used in different contexts. get and gets perform a role similar to that of Haskell's Aeson library: providing a safe way to interact with uncertain data.

Ya, I was thinking something along those lines.

as I detest JSDoc

ha!

@svozza
Copy link
Member Author

svozza commented Mar 5, 2016

You make a convincing case for key in obj, David. Regarding my second point, do you think we should tighten up the definition of Accessible to something like this:

function(x) { return x != null && R.contains(_type(x), ['Array', 'Function', 'Object', 'String']); }

I still have no idea why I can't use S.prop in the file before it's defined even though I declare it as a var (I assumed hoisting would take care of that). It would be nice for use to eat our own dogfood rather than relying on R.prop.

@davidchambers
Copy link
Member

[D]o you think we should tighten up the definition of Accessible[?]

What's the motivation for doing so?

I still have no idea why I can't use S.prop in the file before it's defined even though I declare it as a var (I assumed hoisting would take care of that).

There are two different concepts in JavaScript referred to as "hoisting": var hoisting, and function declaration hoisting.

When one writes…

(function() {
  console.log(x);
  var x = 42;
}());

var hoisting means one has effectively written…

(function() {
  var x;
  console.log(x);
  x = 42;
}());

This will log undefined rather than throw a ReferenceError as one would expect without var hoisting. Note, though, that it does not log 42.

It would be nice for use to eat our own dogfood rather than relying on R.prop.

We could do so by defining prop near the top of the file…

var prop =
def();

then assigning prop to S.prop at the appropriate point in the file.

@svozza
Copy link
Member Author

svozza commented Mar 6, 2016

What's the motivation for doing so?

Because R.prop as it is currently defined will not throw the correct type error if we pass a primitive value in and instead we'll get something like TypeError: Cannot use 'in' operator to search for 'a' in true. If Accessible is supposed to represent types that allow property access then why does it allow primitive types that don't?

@davidchambers
Copy link
Member

Thanks for the explanation, Stefano! I now understand the motivation. :)

If Accessible is supposed to represent types that allow property access then why does it allow primitive types that don't?

Primitive values other than null and undefined support property access:

> true.a
undefined

> 'foo'.toUpperCase()
'FOO'

So Accessible isn't quite right, in this case. We could remove the constraint and determine whether the second argument is valid in the function body. This would enable consistent error messages:

S.prop('x', null);
// ! No ‘x’ property in null

S.prop('x', {});
// ! No ‘x’ property in {}

@svozza
Copy link
Member Author

svozza commented Mar 7, 2016

Primitive values other than null and undefined support property access:

Ah yes, through the magic of autoboxing. Although something like 1.toString() is a syntax error so you have to do 1['toString']()

We could remove the constraint and determine whether the second argument is valid in the function body.

Sounds good, I should be able to work on this later on today.

@davidchambers
Copy link
Member

Although something like 1.toString() is a syntax error so you have to do 1['toString']()

… or (1).toString() or 1..toString(). ;)

@svozza
Copy link
Member Author

svozza commented Mar 7, 2016

1..toString()

Ha. That's a new one, how does that work?

@davidchambers
Copy link
Member

1. is a valid representation of one. The double dot removes the "decimal" ambiguity, I assume.

@svozza
Copy link
Member Author

svozza commented Mar 10, 2016

This implementation got really hairy in the end. To support S.prop('length') for strings I've had to manually autobox the final argument; given that I had to override the linter to do so this all seems a bit wrong.

@davidchambers
Copy link
Member

I don't see a reason to support S.prop('length', 'abc') but not S.prop('toFixed', 42).

We should be able to use the Object function to simplify matters:

-        if (typeof obj === 'string') {
-          /*jshint -W053 */
-          obj = new String(obj);
-          /*jshint +W053 */
-        }
-        if (obj != null && typeof obj !== 'number' &&
-           typeof obj !== 'boolean' && key in obj) {
+        if (obj != null && key in Object(obj)) {

@svozza
Copy link
Member Author

svozza commented Mar 11, 2016

Ah yes, that's much better.

@svozza
Copy link
Member Author

svozza commented Mar 11, 2016

Actually, can we remove the null check and reinstate the Accessible type constraint now?

@davidchambers
Copy link
Member

Actually, can we remove the null check and reinstate the Accessible type constraint now?

Wouldn't that send us back here?

//# prop :: Accessible a => String -> a -> b
//.
//. Takes a property name and returns the value of that property from the
//. specified object; otherwise throws an error.
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?

Takes a property name and a record (an object with known fields of known types), and returns the value of the specified field. If for some reason the record lacks the specified field, a type error is thrown.

For accessing properties of uncertain objects, use get instead.

We should add a "see also" link to the description of get.

@svozza
Copy link
Member Author

svozza commented Mar 11, 2016

Wouldn't that send us back here?

Not if we do something like this:

var boxed = Object(obj);
if (key in boxed) {
  return boxed[key];
...

@davidchambers
Copy link
Member

Ah, I see what you're saying. Sounds great to me!

@svozza
Copy link
Member Author

svozza commented Mar 11, 2016

This is shaping up a lot better now. ⚡

//.
//. Takes a property name and an object and returns the value of the
//. specified field. If for some reason the record lacks the specified field,
//. a type error is thrown.
Copy link
Member

Choose a reason for hiding this comment

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

I suggest two tweaks:

  • "object with known properties" in place of "object"; and
  • "property" in place of "field" (since we no longer mention records).

return boxed[key];
} else {
throw new TypeError('‘prop’ expected object to have a property ' +
'named ‘' + key + '’; ' + R.toString(obj) + ' does not');
Copy link
Member

Choose a reason for hiding this comment

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

When wrapping, I like things to align. Let's use one of the following forms:

throw new TypeError('...' +
                    '...' +
                    '...');
throw new TypeError(
  '...' +
  '...'
);

@davidchambers
Copy link
Member

It's a shame we can't leave custom reactions to comments like ⚡

Haha! With :zap:, though, the fact that a comment generates an email notification is usually desirable.

@svozza
Copy link
Member Author

svozza commented Mar 11, 2016

Actually, that's a good point.

eq(S.prop('0', [1, 2, 3]), 1);
eq(S.prop('length', 'abc'), 3);
eq(S.prop('x', Object.create({x: 1, y: 2})), 1);
});
Copy link
Member

Choose a reason for hiding this comment

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

Let's include a test for an inherited property. For example:

eq(S.prop('global', /x/g), true);

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Although, does Object.create not fulfil that already? x and y live on the created object's prototype.

console.log(Object.create({x: 1, y: 1}));
// => {}

Copy link
Member

Choose a reason for hiding this comment

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

The example I suggested is still useful, but it doesn't test what I was hoping to test since global also exists as an instance property:

> Object.prototype.hasOwnProperty.call(/x/g, 'global')
true

Although, does Object.create not fulfil that already? x and y live on the created object's prototype.

Good point! I didn't realize that Object.create worked this 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.

Yeah it's not obvious, that's why I wrote the test the way I did initially with an object named proto that I then passed into create.

@svozza svozza force-pushed the prop-fn branch 2 times, most recently from 5622161 to f14847b Compare March 14, 2016 18:47
@svozza
Copy link
Member Author

svozza commented Mar 16, 2016

Is there any more that needs to be done on this?

@davidchambers
Copy link
Member

Is there any more that needs to be done on this?

No, but I'd like to do a few other things before merging this pull request:

  1. Update and merge support values of "foreign" types sanctuary-def#38
  2. Publish [email protected]
  3. Submit and merge a pull request to update our sanctuary-def dependency (to address polymorphic functions cannot operate on "foreign" values #165)
  4. Publish [email protected]

I plan to take care of these things this evening. :)

@davidchambers
Copy link
Member

I plan to take care of these things this evening. :)

Actually, I did my tax return this evening instead. :\

@svozza
Copy link
Member Author

svozza commented Mar 17, 2016

Haha. Rather you than me!

@davidchambers
Copy link
Member

Could you rebase this branch, Stefano?

@svozza
Copy link
Member Author

svozza commented Apr 13, 2016

Yes, of course. ⚡

@davidchambers
Copy link
Member

You'll need to update the expected error messages. ;)

While you're at it, you can take advantage of the assert.throws 👽.

@svozza
Copy link
Member Author

svozza commented Apr 13, 2016

Yup, on it!

assert.throws(function() { S.prop('xxx', [1, 2, 3]); },
errorEq(TypeError,
'‘prop’ expected object to have a property ' +
'named ‘xxx’; [1, 2, 3] does not'));
Copy link
Member

Choose a reason for hiding this comment

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

Let's use throws rather than assert.throws for these assertions as well.

Also, now that we're not enforcing a maximum line length let's not wrap the messages. :)

Copy link
Member Author

Choose a reason for hiding this comment

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

@davidchambers
Copy link
Member

🌳 Thank you for persevering with this one, Stefano!

@davidchambers davidchambers merged commit fb92d05 into sanctuary-js:master Apr 13, 2016
@svozza
Copy link
Member Author

svozza commented Apr 13, 2016

A surprisingly tricky one!

@svozza svozza deleted the prop-fn branch April 13, 2016 20:10
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.

3 participants