Closed dmitriz closed 7 years ago
The
{}
looks counter-intuitive
Once you learn that this object holds the type class constraints I think it becomes rather intuitive. Or at least explicit. The empty object shows us that there are no constraints on the type variables.
putting both function arguments and return values inside a single array feels somewhat confusing
I think it's meant to closely resemble Hindley-Milner syntax, where the return value of the function is simply the last item in a list separated by ->
.
the correct signature of this function seems to be
add :: (Number, Number) -> Number
Actually, def
also curries the "implementation". So Number -> Number -> Number
is correct. The fact that it's also possible to call it by passing multiple arguments at once is kind of like a "syntactic sugar" to make it less painful to apply multiple arguments with JavaScript.
Even worse things can happen like placing the curried version into Ramda's
reduce
I'm not sure which "curried version" you are referring to. You shouldn't pass manually curried functions such as your curried add
into def
, as def
will fail to call them properly (assuming that's what you mean?).
If defined via def
correctly, a function can be safely treated as either curried or uncurried. It should therefore be safe to pass it into functions like Ramda's reduce
(which expects (a, b) -> a
) or Sanctuary's reduce
(which expects a -> b -> a
).
I wonder if there any reason not to declare it as
function def({name, constraints, type, impl}) { ...
Sanctuary's API consistently chooses positional function arguments over named arguments (with the exception of create
, after some debate). It's a preference of @davidchambers, I believe. It also makes it easier to curry these functions, since "named arguments" is really just a single argument of an object with mixed value types.
Thanks for the detailed response, @Avaq.
Why positional arguments in this case? Let's consider some Haskell code:
concat :: Semigroup a => a -> a -> a
concat x y = ...
The sanctuary-def equivalent:
const concat = def('concat', {a: [Semigroup]}, [a, a, a], (x, y) => ...);
The function name, type-class constraints, argument types, return type, and implementation appear in the same order in both cases (except that Semigroup a
becomes a: [Semigroup]
). The drawback of positional arguments—the need to choose an argument order—is not applicable as there's a clear order (for those familiar with Hindley–Milner at least).
Is there an advantage to positional arguments over a record argument in this case? Yes. Brevity. The concat
example above would be significantly noisier we were forced to label the constituents. Even the current API (which is as minimal as is possible in JavaScript) is more verbose than we would like (#39).
@Avaq Many thanks for your clarification. I'd like to focus first on the signature issue (or the issue of my wrong understanding 😄 first):
the correct signature of this function seems to be add :: (Number, Number) -> Number
Actually, def also curries the "implementation". So Number -> Number -> Number is correct. The fact that it's also possible to call it by passing multiple arguments at once is kind of like a "syntactic sugar" to make it less painful to apply multiple arguments with JavaScript.
Hm... my function is really auto-curried by def
? I didn't expect it 😄 .
Then how can I pass a function like fetch(url, options)
and use it as fetch(url)
to enjoy the default options? I suppose I would not want it curried in that case.
Even worse things can happen like placing the curried version into Ramda's reduce
I'm not sure which "curried version" you are referring to. You shouldn't pass manually curried functions such as your curried add into def, as def will fail to call them properly (assuming that's what you mean?).
Hm... now I am puzzled. Passing the (manually) curried version of add
would lead to different results? I didn't expect that either. Really confused now. 😕
@davidchambers Thank you for elaborating, I see, it would probably be helpful to mention in the README at least few facts about the signature like that it aims to imitate Hindley-Milner.
I am surely with both hands for brevity, but how would you support functions like fetch
taking optional arguments? That would mean, you can't auto-curry them of course, so is it done via some setting in the constraint argument?
Hm... my function is really auto-curried by
def
?
That's right. def
is like R.curry
with optional type checking.
Then how can I pass a function like
fetch(url, options)
and use it asfetch(url)
to enjoy the default options? I suppose I would not want it curried in that case.
Actually, this is exactly the reason to curry functions in the first place: to allow more specific functions to be defined by partial application of a more general one. This is what I suggest:
// defaultOpts :: FetchOptions
const defaultOpts = {...};
// fetch :: FetchOptions -> String -> Future String String
const fetch =
def('fetch',
{},
[FetchOptions, $.String, $Future($.String, $.String)],
(opts, url) => {...});
// get :: String -> Future String String
const get = fetch({method: 'GET'});
Hm... now I am puzzled.
What Aldwin was saying is that the implementation provided to def
should be something like (opts, url) => {...}
rather than opts => url => {...}
.
<3 this:
const get = fetch({method: 'GET'});
@dmitriz take a look at:
they demonstrate how data-last approach with currying is great.
@davidchambers I see, yes, that sounds like a better and safer way. So the def
provides the complete Ramda's curry
functionality? Which feels like "over-currying" ;). Would be nice to mention in the Readme somewhat explicitly.
What Aldwin was saying is that the implementation provided to def should be something like (opts, url) => {...} rather than opts => url => {...}.
Hm... if passing the curried function opts -> url -> ...
directly leads to a different outcome, I wonder what is the intended behaviour in that case and the reason for that?
Would be nice to mention in the Readme somewhat explicitly.
Is this not explicit?
sanctuary-def is a run-time type system for JavaScript. It facilitates the definition of curried JavaScript functions which are explicit about the number of arguments to which they may be applied and the types of those arguments.
Applying
add
to two arguments gives the expected result:add(2, 2); // => 4
Applying
add
to fewer than two arguments results in a function awaiting the remaining arguments. This is known as partial application. Partial application is convenient as it allows more specific functions to be defined in terms of more general ones:// inc :: Number -> Number const inc = add(1); inc(7); // => 8
Hm... if passing the curried function
opts -> url -> ...
directly leads to a different outcome, I wonder what is the intended behaviour in that case and the reason for that?
When currying a binary function via def
, the function provided must be of type (a, b) -> c
rather than a -> b -> c
. This was chosen as it's much more convenient when one does not have access to arrow functions. I'm strongly opposed to ad hoc polymorphism so inspecting the function provided in an effort to support both forms is off the table. You could define and use an uncurry
function if you find yourself with lots of manually curried functions you'd like to def
.
Would be nice to mention in the Readme somewhat explicitly.
Is this not explicit?
I can only see it at the very beginning:
sanctuary-def is a run-time type system for JavaScript. It facilitates the definition of curried JavaScript functions which are explicit about the number of arguments to which they may be applied and the types of those arguments.
Which can be easily misread as it takes curried functions and makes them safe by enforcing the number of arguments. That is how I read it, which is apparently wrong.
Something along the lines, that it actually requires a plain uncurried function, but produces its fully curried "sister" with full Ramda's functionality (if it is?) would have avoided the misunderstanding for me.
Not meant as criticism (the readme is awesome otherwise), just a suggestion to improve it.
When currying a binary function via def, the function provided must be of type (a, b) -> c rather than a -> b -> c. This was chosen as it's much more convenient when one does not have access to arrow functions. I'm strongly opposed to ad hoc polymorphism so inspecting the function provided in an effort to support both forms is off the table. You could define and use an uncurry function if you find yourself with lots of manually curried functions you'd like to def.
Yes, without arrow function that is more tedious indeed. I also now can see the reason behind your decision. I might not be the last one to come back asking about it though, so some sort of FAQ might smoothen it for you to have to explain it again. 😄
Something along the lines, that it actually requires a plain uncurried function, but produces its fully curried "sister" with full Ramda's functionality (if it is?) would have avoided the misunderstanding for me.
I just opened #116. Hopefully it clarifies the behaviour of def
.
Why Ramda? Favoring Curry Hey Underscore, You're Doing It Wrong! they demonstrate how data-last approach with currying is great.
@safareli Thanks, great links! Ironically Ramda's reduce
actually requires the uncurried function and does not work with its curried version, as one might expect. ;)
I wonder if there is any reason for choosing this signature: https://github.com/sanctuary-js/sanctuary-def/blob/master/index.js#L62
The
{}
looks counter-intuitive (unless one goes deep into the code, of course, and reads the definitions of the variables, which, however, are not visible from the function invocation in this text aimed at the newcomers).Also putting both function arguments and return values inside a single array feels somewhat confusing as their meaning is fundamentally different. (Even mathematically, you would have to switch to duals when moving variables between source and target of a function.)
But even more importantly, the correct signature of this function seems to be
which is not the same as the curried one above, and works differently in JavaScript. Even worse things can happen like placing the curried version
into Ramda's
reduce
, which leads to different (and less intuitive) results than its expected uncurried sisterSo the question arises, which of these two signatures is reflected in this declaration:
Looking at the
def
definition https://github.com/sanctuary-js/sanctuary-def/blob/master/index.js#L2346, I wonder if there any reason not to declare it asThen
would become
whereas
would become
and the more complex signature
would become
or, the real world example
would become