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

abandon Ramda-style currying in favour of simple currying #179

Merged
merged 1 commit into from
Apr 1, 2018

Conversation

davidchambers
Copy link
Member

@davidchambers davidchambers commented Dec 17, 2017

Commit message:

def has served two roles:

  • currying an uncurried implementation function; and
  • performing type checking at run time.

At the time of this project's inception in 2015 it seemed unreasonable to require users to provide curried implementation functions. Defining curried functions manually was onerous prior to ES6:

function(x) {
  return function(y) {
    return function(z) {
      return ...;
    };
  };
}

With ES6, defining curried functions is trivial:

x => y => z => ...

The landscape has changed since 2015, and it's now reasonable to assume that users are targeting ES6. Furthermore, the advantages of Ramda-style currying over simple currying do not justify the additional complexity. There's no compelling reason for def to continue to serve two roles.

User-facing changes:

  • $ functions must now be applied to arguments one at a time;
  • def now requires a curried implementation function;
  • def no longer imposes an arbitrary arity limit;
  • def functions must now be applied to arguments one at a time; and
  • __ has been removed (simple currying precludes placeholders).

Internal changes:

  • the checkTypes option is now checked in just one place; and
  • def is now essentially a no-op if type checking is disabled.

This pull request embraces the f (x) (y) style proposed in sanctuary-js/sanctuary#438.

The changes to the test suite will be easier to review once #178 has been merged.

index.js Outdated
@@ -58,23 +58,23 @@
//.
//. ```javascript
//. // env :: Array Type
//. const env = $.env.concat([List($.Unknown)]);
//. const env = $.env .concat ([List ($.Unknown)]);
Copy link
Member

Choose a reason for hiding this comment

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

Inconsistency: Space preceding .concat but not .env.

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 space after the method name is to appease the linter. If we want to write f (x) the linter wants us to write foo.bar (baz) as well. I was experimenting with surrounding method names with spaces to make them appear more like infix operators: xs .concat (ys). What do you think?

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 see a :thumbsdown: from Gabe. I certainly don't love the proposal. Let's vote!

  • 😆 $.env.concat([List ($.Unknown)])
  • 🎉 $.env.concat ([List ($.Unknown)]) ✔︎
  • ❤️ $.env .concat ([List ($.Unknown)]) ✔︎

The ✔︎/✘ indicates whether we'll be able to continue to use func-call-spacing.

Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of the formatting as a signal that curried functions are being used.

[1, 2, 3].concat([4, 5, 6]); // Nothing special going on here
S.concat ([1, 2, 3]) ([4, 5, 6]); // FP in the house!

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 can see the logic behind that, Gabe. What about unary functions, though? Math.abs(x) or Math.abs (x)? S.trim(s) or S.trim (s)?

Copy link
Member

@Avaq Avaq Dec 20, 2017

Choose a reason for hiding this comment

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

What about require('express')(b, c)? What about fs.reduce(f, x)(y)? I think trying to categorise function calls based on underlying structure will raise more questions than it settles. I vote for the consistency of always putting a space between the function or method identifier and its application:

  • Math.abs (x)
  • require ('express') (a, b)
  • fs.reduce (f, x) (y)
  • $.env.concat ([List ($.Unknown)])

Copy link
Member Author

Choose a reason for hiding this comment

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

f(x)(y).z is problematic. In f (x) (y).z the .z property access appears to be bound to (y) rather than to f (x) (y). There are two reasonable solutions, each of which violates one of our current rules:

I prefer the latter as it is consistent with foo.bar.baz (I'm not ready to switch to foo .bar .baz).

Copy link
Member

Choose a reason for hiding this comment

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

So with this new approach, all preceding occurrences of Z.env .concat can be fixed, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

@@ -402,6 +393,10 @@
var Inconsistent =
new _Type(INCONSISTENT, '', '', always2('???'), K(false), [], {});

// NoArguments :: Type
var NoArguments =
new _Type(NO_ARGUMENTS, '', '', always2('()'), K(true), [], {});
Copy link
Member

Choose a reason for hiding this comment

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

If I understand correctly what this is for, we could consider naming it Unit, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

NoArguments and Unit represent related but distinct ideas.

Were we to define const Unit = {}; and the accompanying Type value, f () and f (Unit) would still be different expressions: one applies f to no arguments; the other applies f to one argument.

It could be confusing to name the above Type value Unit as we may one day wish to define a Type value equivalent to Haskell's (). This would logically be named Unit.

Copy link
Member

@Avaq Avaq Dec 20, 2017

Choose a reason for hiding this comment

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

In Haskell () is a nullary tuple. I like to think that the only differences between Haskell and JavaScript, are that JavaScript forces you to pattern-match your tuples, and you can only instantiate them at call time.

//    f :: Tuple a b -> c -> ...
//      -- alernatively written as
//    f :: (a, b) -> c -> ...
const f = (a, b) => c => ...

When viewing it like this, Unit has no addressable value level member (only at call time).

//. type; and
//. - `$.Function([a, b, a])` represents the `(a, b) -> a` type.
function Function_(types) {
//. - `$.Function ([a, b, a])` represents the `(a, b) -> a` type.
Copy link
Member

Choose a reason for hiding this comment

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

Does it still?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. The behaviour of $.Function is unchanged. I believe sanctuary-def should be capable of expressing types of which we disapprove. ;)

index.js Outdated
function NullaryType(name) {
return function(url) {
return function(test) {
function format(outer, inner) {
Copy link
Member

Choose a reason for hiding this comment

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

It looks like the format function could be shared between different applications of NullaryType (name).

Copy link
Member Author

Choose a reason for hiding this comment

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

Great point! I'll address this.

Copy link
Member Author

Choose a reason for hiding this comment

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

return function(test) {
return function(_1) {
return function($1) {
function format(outer, inner) {
Copy link
Member

Choose a reason for hiding this comment

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

ditto

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 function references $1.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I missed that.

return function(_2) {
return function($1) {
return function($2) {
function format(outer, inner) {
Copy link
Member

Choose a reason for hiding this comment

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

ditto

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 function references $1 and $2.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I missed that.

return new _Type(VARIABLE, name, '', format, K(true), keys, types);
return function($1) {
return function($2) {
function format(outer, inner) {
Copy link
Member

Choose a reason for hiding this comment

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

ditto

index.js Outdated
}

// fromUncheckedBinaryType :: ((Type, Type) -> Type) ->
// fromUncheckedBinaryType :: (Type -> Type -> Type) ->
// (Type -> Type -> Type)
Copy link
Member

Choose a reason for hiding this comment

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

Brackets for clarity?

Copy link
Member Author

Choose a reason for hiding this comment

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

([a, a, a, $.Array (a)])
(x => y => z => [x, y, z]);

// $26 :: a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> Array a
Copy link
Member

Choose a reason for hiding this comment

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

Haha

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 wanted to show that we're no longer limited to nine parameters. 😂

test/index.js Outdated
});
// eq :: (a, b) -> Undefined !
function eq(actual, expected) {
assert.strictEqual (arguments.length, eq.length);
Copy link
Member

Choose a reason for hiding this comment

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

Curious that you use the new style in the examples and the tests, but not the actual source.

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 can imagine reformatting the source code too, but I'd prefer to do so in a separate pull request to avoid obfuscating the significant code changes.

@davidchambers davidchambers force-pushed the davidchambers/simple-currying branch from 2b72b37 to e16c7b0 Compare December 26, 2017 18:41
index.js Outdated
} else {
indexes.push(index);
return function(x) {
var args = slice.call(arguments);
Copy link
Member

Choose a reason for hiding this comment

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

I think we might be able to avoid calling slice.call here. It would increase performance.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great point!

}

var isThunk = expType.types.$1.type.type === NO_ARGUMENTS;
var numArgsExpected = isThunk ? 0 : expType.keys.length - 1;
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: I want to think about this approach.

Copy link
Member

Choose a reason for hiding this comment

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

My thoughts: #180

index.js Outdated
vReprs.push(Z.toString(_values[idx]));
var wrapped = typeInfo.types[0].type === NO_ARGUMENTS ?
function() {
var args = slice.call(arguments);
Copy link
Member

Choose a reason for hiding this comment

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

Same here. We can move the slice.call part to the unhappy path.

@Avaq Avaq mentioned this pull request Dec 28, 2017
@davidchambers davidchambers force-pushed the davidchambers/simple-currying branch 2 times, most recently from d960c05 to 9997c9c Compare March 5, 2018 02:40
@davidchambers
Copy link
Member Author

I've rebased this pull request now that #184 has been merged. Please review scripts/lint and .eslint-es6.js.

Copy link
Member

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

Looks good to me!

@davidchambers davidchambers force-pushed the davidchambers/simple-currying branch 4 times, most recently from 2e6779f to 51ff546 Compare March 31, 2018 11:43
‘def’ has served two roles:

  - currying an uncurried implementation function; and
  - performing type checking at run time.

At the time of this project's inception in 2015 it seemed unreasonable
to require users to provide curried implementation functions. Defining
curried functions manually was onerous prior to ES6:

    function(x) {
      return function(y) {
        return function(z) {
          return ...;
        };
      };
    }

With ES6, defining curried functions is trivial:

    x => y => z => ...

The landscape has changed since 2015, and it's now reasonable to assume
that users are targeting ES6. Furthermore, the advantages of Ramda-style
currying over simple currying do not justify the additional complexity.
There's no compelling reason for ‘def’ to continue to serve two roles.

User-facing changes:

  - ‘$’ functions must now be applied to arguments one at a time;
  - ‘def’ now requires a curried implementation function;
  - ‘def’ no longer imposes an arbitrary arity limit;
  - ‘def’ functions must now be applied to arguments one at a time; and
  - ‘__’ has been removed (simple currying precludes placeholders).

Internal changes:

  - the ‘checkTypes’ option is now checked in just one place; and
  - ‘def’ is now essentially a no-op if type checking is disabled.
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