-
-
Notifications
You must be signed in to change notification settings - Fork 94
Description
Sanctuary is defined in part by what it does not support. We have done a good job of managing complexity and entropy, and we must continue to do so if Sanctuary is to live a long, healthy life.
Ramda-style currying—the ability to write both f(x)(y)
and f(x, y)
—is a source of complexity. I've seen this complexity as necessary to prevent code written with Sanctuary from looking strange to newcomers, which would limit the library's initial appeal and thus limit the library's adoption.
Last night it occurred to me that we could possibly solve (or at least mitigate) the ")(
" problem by tweaking the way in which we format function applications.
The ")(
" problem
In JavaScript, this reads very naturally:
f(x, y, z)
This, on the other hand, seems unnatural:
f(x)(y)(z)
A day ago my impression was that the only aesthetic problem was having opening parens follow closing parens. I now see a second aesthetic problem, as I hope this example demonstrates:
f(g(x)(y))(h(z))
There's no space. There's a significant difference visually between x)(y
and x, y
. The nesting of subexpressions above is not immediately clear to a human reader. When we include space between arguments—as is common practice in JavaScript—the nesting is clear:
f(g(x, y), h(z))
This clarity is the primary benefit of Ramda-style currying. I consider S.concat(x)(y)
bad style not because of the )(
but because if used consistently this style results in expressions which are less clear than their more spacious equivalents.
It's worth noting that multiline function applications are also natural with the comma style:
f(x,
y,
z)
x
, y
, and z
are obviously placeholders for longer expressions in this case.
Here's the )(
-style equivalent:
f(x)
(y)
(z)
My concern is that visually the x
is more tightly bound to f
than it is to y
and z
, making the first argument feel privileged in some way.
Learning from Haskell
Sanctuary brings many good ideas from Haskell to JavaScript. Perhaps most important is the combination of curried functions and partial application. We might be able to learn from Haskell's approach to function application.
In Haskell, function application is considered so important that a space is all it requires syntactically: f x
in Haskell is equivalent to f(x)
in JavaScript. The associativity of function application is such that f x y
is equivalent to (f x) y
, which is to say that what we write as f(x)(y)
in JavaScript could simply be written f x y
in Haskell.
Let's consider how the previous examples would look in Haskell:
f x y z
f (g x y) (h z)
f x
y
z
All three Haskell expressions are less noisy than both of their JavaScript equivalents. Note that in the second expression it's necessary to use parens. We'll return to this idea shortly.
A small change can make a big difference
The proposal:
When applying a function, include a space before the opening paren.
This means we'd write f (x)
rather than f(x)
, and f (x) (y)
rather than f(x)(y)
. This gives expressions breathing room they lack when formatted in the )(
style.
Let's revisit the examples from earlier to see the formatting tweak in action.
f (x) (y) (z)
This looks odd to me now, but I think it could become natural. The key is to see the spaces as the indicators of function application (as in Haskell) and the parens merely as grouping syntax for the subexpressions. It's interesting to note that the code above is valid Haskell.
f (g (x) (y)) (h (z))
Again, this is valid Haskell with "unnecessary" grouping around x
, y
, and z
. The spaces make it easier for me to determine that f
is being applied to two arguments (one at a time). This would be even clearer if the arguments were written on separate lines:
f (g (x) (y))
(h (z))
One could even go a step further:
f (g (x)
(y))
(h (z))
This leads quite naturally to the original multiline example:
f (x)
(y)
(z)
The space is advantageous in this case too, separating x
from f
so x
binds more tightly, visually, with the other arguments than with the function identifier.
Realistic example
Here's a function from sanctuary-site, as currently written:
// version :: String -> Either String String
const version =
def('version',
{},
[$.String, Either($.String, $.String)],
pipe([flip_(path.join, 'package.json'),
readFile,
chain(encaseEither(prop('message'), JSON.parse)),
map(get(is(String), 'version')),
chain(maybeToEither('Invalid "version"'))]));
Here's the function rewritten using the proposed convention:
// version :: String -> Either String String
const version =
def ('version')
({})
([$.String, Either ($.String) ($.String)])
(pipe ([flip_ (path.join) ('package.json'),
readFile,
chain (encaseEither (prop ('message')) (JSON.parse)),
map (get (is (String)) ('version')),
chain (maybeToEither ('Invalid "version"'))]));
Here's a Lispy alternative which makes the nesting clearer:
// version :: String -> Either String String
const version =
def ('version')
({})
([$.String, Either ($.String) ($.String)])
(pipe ([flip_ (path.join)
('package.json'),
readFile,
chain (encaseEither (prop ('message'))
(JSON.parse)),
map (get (is (String))
('version')),
chain (maybeToEither ('Invalid "version"'))]));
I like the comma style best, although I can imagine growing to like the proposed convention. Even if we decide that the proposed convention makes code slightly less easy to read we should consider adopting it in order to reap the benefits outlined below.
Benefits of replacing Ramda-style currying with regular currying
Although this proposal is focused on an optional formatting convention, it is motivated by the desire to simplify. If we decide that the proposed convention addresses the readability problems associated with )(
style, we can replace Ramda-style currying with regular currying. This would have several benefits:
-
Simpler mental model. When learning Sanctuary or teaching it to others one would not need to read or explain the interchangeability of
f(x)(y)
andf(x, y)
for Sanctuary functions. -
One and only one. There would be a single way to express function application (the Haskell way). When writing code one would no longer be distracted by wondering whether
f(x, y)
is more efficient thanf(x)(y)
. Teams would not need to choose one style or the other (although there may still bef(x)
versusf (x)
debates). -
Agreement between code examples and type signatures. Our type signatures indicate that Sanctuary functions take their arguments one at a time, but our examples currently use comma style which could be leading readers to believe that our type signatures are inaccurate.
-
Simpler implementation. The currying code in sanctuary-def would become significantly simpler if it only needed to account for
f(x)(y)(z)
.
Poll
I'd love to know where you stand on this.
Reaction | Meaning |
---|---|
❤️ | I already use f(x)(y) or f (x) (y) exclusively. |
👍 | I currently use f(x, y) but this proposal has encouraged me to adopt f(x)(y) or f (x) (y) . |
😕 | I prefer f(x, y) but find the arguments for dropping Ramda-style currying compelling. I would adopt f(x)(y) or f (x) (y) if necessary. |
👎 | I prefer f(x, y) and want Sanctuary to continue to use Ramda-style currying. |
Feel free to vote based on your first impressions but to change your vote if you change your mind.