-
-
Notifications
You must be signed in to change notification settings - Fork 95
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
readable code without Ramda-style currying #438
Comments
love the rationale @davidchambers I've taken to real/manual currying in my own code base. My reasons were static analysis by the editor, a better debugging experience, and (assumed) performance. But I've never thought of changing the whitespace to avoid the )( "butts" 😄 I'm for it, even if it feels foreign to me right now. In your final example I like the "proposed" syntax more than the lispy version. note: I voted ❤️ even though I don't use manual currying exclusively, I do use it in a lot of contexts, and it was the best fitting option there for me. |
Consider me a highly enthusiastic supporter of this proposal. I began switching to a very similar style a few months after I started incorporating Ramda in my toolbox, and as @JAForbes does, I also, increasingly, curry my functions manually. I find this style to be an excellent fit for functional code. The rest of my style is a variation on that of the npm team. Here's a sample snippet lifted out of some code I'm currently working on ( const getValue =
ifte( isConstructed )
( prop( 'value' ) )
( id ) As a follow up, here's how your // 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"' ) )
]
)
) Certainly not to everyone's taste, but I find that this style gives a very clear overview of nesting in complex functional code. |
I've been thinking about what changes might go in a Ramda 2.0 if we ever get Ramda 1.0 out the door, and one of the best simplifications I can imagine is to drop all support for polyadic functions. This of course would make it more Haskell-like, and I think would gel well with this Sanctuary proposal. But I'm stuck on one thing. Keeping Fantasy-land compliance would seem to necessitate being able to support certain binary functions, for Foldable, Bifunctor, and Profunctor, and perhaps others. So I like this proposal, and could see doing something similar with Ramda one day, I'm not quite sure how that could play out. |
When I first read this, my thinking was 👎 and here is why: I am working on a large codebase that dates back to 2013 (maybe older in places)…it has been through 3 distinct teams, none of the original authors are around anymore. I've slowly been introducing new concepts to clean it up (started with Ramda, then Fluture and recently Sanctuary)…and it's been a slow process. Because of the changes over the years we have a very clear lava layer effect happening. So my initial thought was… If I now decide to change the way we call our functions then that would be yet another lava layer on top of what I've already introduced…another debt to pay. Introducing new libraries and concepts such as currying has something that I've done very deliberately and with much forethought regarding my future peers (until recently I've been the sole front end developer on this project). But, reading the proposal again (and taking more time to do so), I decided to look at the places where I'm using currying and I don't see a large impact (if any) with this new style. It turns out most of the functions I use have an arity of 2 or 3. I agree with the rationale. The examples look nice, I prefer the first proposal. I've officially voted 👍 , however I am concerned a little concerned about enforcing it my current project. |
The methods of the Sanctuary data types will need to remain uncurried, but there's no reason that Sanctuary's function-based API must follow suit. Perhaps, Scott, you're imagining wanting to pass As often seems to be the case, decisions in the Ramda world involve more complicating factors than decisions in the Sanctuary world. In this case, since |
Terrific feedback, @miwillhite. I particularly appreciate the link to the Lava Layer post as I've been making a similar argument at work and I now have a catchy name for this idea:
Upgrading to a version of Sanctuary which no longer provided Ramda-style currying would not be trivial, but there are a couple of things that could assist with the transition:
I'm confused by this. Sanctuary functions of arity 2 or 3 would be affected by the change unless you're already using the |
An example: // fn1 :: a → b → c
// fn2 :: a → b → c → d
// Where currently I'd have something like this:
pipe([
f1(a),
f2(a, b),
])(foo);
// With the change, (spacing arguments aside) I wouldn't need to change the way I call f1
// and f2 would only have a slight difference:
pipe([
f1(a),
f2(a)(b),
])(foo);
// With spacing adjustments I see a lot more impact…something like
pipe ([
f1 (a),
f2 (a) (b),
])
(foo); |
Yes, and I'm not worried about transitional issues. If Ramda went all the way to unary-only functions, we'd muddle through the transition. But I would worry about, say, delegating a |
To my mind, the LISP-style indentation is not the key. This would still be compatible: pipe([
f1 (a),
f2 (a) (b),
]) (foo); But maybe I'm misunderstanding David's suggestion. |
@CrossEye you are correct…I took some liberties in my response to play around with what it might look like in the "ideal" state ;) |
Seems like I'm in minority here but I'm a 👎 on this, Sanctuary is opinionated enough without now telling me how to format my code. I reckon I'd have a mutiny on my hands if I tried to introduce this to my team because I couldn't defend a library imposing something as subjective code style. I'm also not convinced by the points in the benefits section: 1 and 3 are complete non-issues imo because if they were real problems we'd have had at least one person mention them in Gitter or file a bug report but they never have. 2 is just a matter of personal preference, for instance, I've no problem with there being more than one way to do things in programming: it strikes me as an utterly natural consequence of the variety of cognitive models different human beings possess and yet people have written whole books arguing the opposite. Who's to say who's right? The only compelling argument for me is the simpler implementation of |
We do need to transform function reduce(f, initial, foldable) {
return Z.reduce(uncurry2(f), initial, foldable);
} For people unfamiliar with the Fantasy Land spec there's no confusion. I can imagine authors of Fantasy Land -compliant data types assuming that S.chainRec :: ChainRec m => TypeRep m -> (a -> m (Either a b)) -> a -> m b chainRec :: ChainRec m => ((a -> c, b -> c, a) -> m c, a) -> m b |
Thanks for the example, @miwillhite. That's very helpful. I see the point you're making. Given that you're often writing pipelines, one argument is often provided later. This means you're only providing two arguments at once to functions which take at least three arguments, which are not common. |
Thank you for sharing your thoughts, @svozza. Dissenting viewpoints are particularly valuable. Out of interest my vote is 😕 (I selected all four options initially to make voting easier for others). I've been surprised by the enthusiasm with which this issue has been greeted overall.
To be clear, I'm not suggesting that we dictate how anyone format their code (aside from Sanctuary contributors, of course 😜). As Sanctuary grows it will naturally become incrementally more complex; I'm keen to find ways to offset this. Removing Ramda-style currying would reduce complexity, but I've considered the costs to be too high. Since I have reservations about giving up my beloved commas, it's easy imagine others being surprised or upset if This issue is about exploring the possibility of embracing the simpler currying that @JAForbes and @kurtmilam now use in their projects. It's as much about seeing whether I can convince myself that I could live happily in a comma-free world as it as about me convincing others. Furthermore, our comma-free worlds needn't look the same. Mine may involve Stefano, could a mutiny be prevented if |
Another potential point in favor of the simpler style of currying is simplifying TypeScript typings. I'm a TypeScript noob, but my cursory research suggests that it's not yet trivial to type a variadic It seems, on the other hand, that creating typings for manually curried functions is already trivial: function add(x: number): (y: number) => number {
return function(y: number): number {
return x + y;
}
} |
Sorry, I was being hyperbolic but I find the Regarding the script, I guess that could be a good compromise but it also means that I then have to introduce a build step and one of the things that Sanctuary has taught me to appreciate is how nice it is to be able to eschew all that. It was only when I saw the contortions others (just look at the ES6 PR in Ramda for example, for example) on the frontend have to go through with Babel etc that I realised how easy my life was having only ever used Node.js for backend development. I would be reticent give that up. |
@kurtmilam: yeah, that sounds accurate. function add(x: number) {
return (y: number) => x + y;
} Then again, the inference would break down with e.g. iteration. fwiw on this proposal I'd have a slight preference toward pro-choice for user-friendliness. anecdotally, my current style had been |
I wasn't clear. I'm proposing a script to be run once per project to update files in place:
It would save each of us from performing the |
Ah sorry, I thought you meant we'd provide a script for people to run as part of their build that allows them to use the old style, like a Babel transform. Yeah, for the Sanctuary project I just assumed we'd script it, it would be such a pain to do manually. |
I voted 👍 not necessarily because of the specific arguments, but more from a general point about clarity and the learning process. I say this as a relative newbie who still struggles with the basics. Most of the friction I've experienced in switching to more functional-programming inspired code is due to mixing new ideas with old habits. I look forward to breaking those old habits and learning new ways of writing (and hence thinking). Really, if I could hit a button and make that transition now I would. The reality is though that I kindof need to be forced into it, otherwise I'll naturally follow the path of least resistance, at least some of the time. If |
In other words, I think this:
Will be resolved by a larger movement of programmers who are using functional programming in javascript to deliver better code and meet managements expectations more thoroughly. Word will spread and the results will speak for themselves. To accomplish that, the library genuinely needs to be the best it can be to deliver results, and imho I think that should be the primary focus. It's of course easier to say that when I have no ownership/responsibility of the library - but I genuinely think programmers are an adaptable bunch who will learn what they need to in order to be more productive. Javascript in general requires a frightening amount of adaptability to survive in the current ecosystem, much moreso than other languages imho or at least in different ways. Coming off of a background in Unity-c#/flash-as3/Go/C - the learning path was always about building on previous knowledge. Only in javascript am I finding that I have to keep going back and learning new fundamentals, like the rug is continually being pulled out from under me - and it seems like it's just part of the game here. To be an effective javascript programmer you kindof need to get used to that. That's my experience at least after moving to the browser around half a year or so ago. |
Kindof going on a rant here so I'll stop soon - but one last point, while I think the focus of the library itself should be primarily based on the best technical decisions, I'd say the exact opposite for the documentation and tutorials. A very clear explanation that assumes zero prior knowledge and gets people used to the "new" style would be far more powerful than supporting both styles. That's just my opinion though, take with a grain of salt :) (fwiw this is already a known issue in general, e.g. #210 and #419) |
I've created sanctuary-js/sanctuary-def@master...davidchambers/simple-currying in order to see how much complexity we could remove from sanctuary-def's internals were we to abandon Ramda-style currying. It's beneficial (in my view) that a curried function would report its It's worth having a look at the diff to see the |
On TypeScript: Personally, rather than let the current limitations of TS influence the functionality of the library as a whole, I would opt to restrict the TS definitions to the subset of functionality they can easily express. Variadic interfaces already have no choice but to do this. Additionally, TS is moving fast and likely to be in a very different place 2-3 years from now. By then it may have expanded to support typing anything you would have dropped because of it. Not to rag on TS on too much, but before even getting to currying it's already in fairly poor shape to safely handle higher order functions due to parameter bivariance. That plus inferred generics is a massive foot gun. Unless you're writing for TypeScript first, this has the same answer as the above: Shape the types as best you can, then throw up your hands and wait for improvements. On the readability of simply curried functions: Purely subjective, but as soon as I see a pointfree thing that's at all hard to read my first reaction is FP Extract Method rather than worrying about whitespace. I think the formatting alternatives above are all reasonable, but I'd use 3-5x the declarations myself. If moving to simple currying, you might also consider keeping completely non-curried versions around. No opinion on whether it's the same function or not, but dealing with a single polyadic interface is pretty trivial. The net result would be There may be another middle path approach like the above that ends up being a win. I'm new to the library so grain of salt! |
I like this approach. It suggests that regardless of what we eventually decide here we could provide curried type definitions only in #431. By simplifying our requirements we could release something sooner, then decide whether to take on the complexity of handling all the other combinations.
Your perspective as someone new to the library is invaluable. |
I'm fairly familiar with Ramda but haven't written anything major in it yet*, but haven't sat down and learned Sanctuary even though my vague impression is that it's type-strict Ramda and some companion Fantasy-Land libraries. I've got sort of lava layers in my own habits inasmuch as, like someone else here mentioned, I'm trying to learn to apply functional ideas but am still in the process of learning. I really like the idea of partial application since reading about the whole data-last thing and seeing examples of how much more composition Ramda enables, but it took me a while to get to where I feel like currying -- everything being partial application anyway -- is the "clean" way to do it (although it certainly helped that JavaScript's explicit partial application functions are all... clunky). In my mind at this point in my developer growth, here's how I feel about syntax:
My initial reaction to the recently suggested idea of providing both uncurried and simply curried versions of the functions, with one automatically derived from the other, is that it seems great; but, on the other hand, I don't happen to know whether there's a ton of advantage of Ramda or Sanctuary over, say, Lodash or Underscore with arguments switched around, when not taking advantage of the currying at all. I do think using EDITTED TO ADD: Is/are there (a) good linting rule(s) for the proposed style? If not, is anyone here familiar enough with writing linting rules that (a) rule(s) for the proposed style could be made? (Perhaps even something like "if it's not curried [or takes multiple arguments if not-curried can't be detected by the linter] then allow other, 'traditional' spacing rules; if it's curried [or, has a series of one-argument calls, if currying can't be detected by the linter directly] require {the proposed style}"...) I say this as a guy who hasn't yet gotten comfortable with actually linting his projects simply because there are a few things I tend to do that clash with more common linting rules and I haven't yet learned to write my own variant for my preferred techniques, but in theory the proposal seems like exactly the sort of thing that would be helpful to be able to automatically handle (e.g. with a rule that can not just detect violations but fix them). * I was very tempted to write my current project in Ramda as much of it is just data transformation, but I felt at the time that if I went with Ramda I'd wind up trying to make everything point-free, and didn't know who I'd be partnering with to maintain it in the future, so I decided to look for some kind of compromise instead of counting on being able to teach a future colleague how to read point-free style in order to understand any of the codebase at all. |
Function composition, |
Ha, can't believe I forgot about I guess that's pretty clear then -- there are at least some uses that might be worth exposing the functions in an entirely uncurried version in addition to a hypothetical simply curried version, regardless of whether Ramda-style currying is kept or dropped! |
The great things about Ramda's style currying function is it's readable, and |
I hadn't seen maybe callbacks, but one approach is to put all arguments in
an object with some keys optional.
…On Dec 18, 2017 10:47 AM, "David Komer" ***@***.***> wrote:
Re: commas, I find it also removes some friction in thinking where to
place them for multi-line calls. One less thing to think about :)
One issue I've come up against though is for optional parameters. Is the
idiomatic way to deal with it to *always* wrap in a Maybe? e.g.
const foo = arg => maybeCallback => {
//do stuff with arg, get result
S.map (c => c(result)) (maybeCallback);
}
foo (bar) (S.Nothing);
foo (baz) (S.Just(myCallback));
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#438 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AC6uxZAPtsNNBh-xBoz5xP86-F-r8-Heks5tBjSjgaJpZM4PKLTy>
.
|
Is there a https://prettier.io configuration for this function application spacing style? |
I just noticed https://github.com/joelnet/eslint-config-mojiscript which seems to achieve this style with eslint. |
If you find a solution that works for you, @onetom, please share it here. We could then add it to the readme and website. |
I can confirm that
.eslintrc.js
UPDATE: The eslint rule that allows for this to be automatically formatted is func-call-spacing. Should be as easy as adding: .eslintrc.js
Typescript users can make use of the same auto-fix by ditching tslint in favor of eslint as TS has already announced they intend to only support eslint in the future. The setup is here. Then, same rule except Also, WebStorm supports an auto-fix through the built-in options |
Thank you for sharing this information, @RichardForrester. |
@RichardForrester did you figure out how to make this rule work with prettier? |
@alexandermckay It’s been a while since I messed with this, but from memory you don’t really need the mojiscript to use the function-call-spacing rule, and as far as working with prettier, I usually bind a command to first run prettier and then run eslint auto-fix. |
@RichardForrester thank you for the tips! It was actually quite simple to get them to work together. I have created a repo which shows how to set this up for anyone else using a combination of |
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)
andf(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 "
)(
" problemIn JavaScript, this reads very naturally:
This, on the other hand, seems unnatural:
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:
There's no space. There's a significant difference visually between
x)(y
andx, 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: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:
x
,y
, andz
are obviously placeholders for longer expressions in this case.Here's the
)(
-style equivalent:My concern is that visually the
x
is more tightly bound tof
than it is toy
andz
, 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 tof(x)
in JavaScript. The associativity of function application is such thatf x y
is equivalent to(f x) y
, which is to say that what we write asf(x)(y)
in JavaScript could simply be writtenf x y
in Haskell.Let's consider how the previous examples would look in Haskell:
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 thanf(x)
, andf (x) (y)
rather thanf(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.
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.
Again, this is valid Haskell with "unnecessary" grouping around
x
,y
, andz
. The spaces make it easier for me to determine thatf
is being applied to two arguments (one at a time). This would be even clearer if the arguments were written on separate lines:One could even go a step further:
This leads quite naturally to the original multiline example:
The space is advantageous in this case too, separating
x
fromf
sox
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:
Here's the function rewritten using the proposed convention:
Here's a Lispy alternative which makes the nesting clearer:
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.
f(x)(y)
orf (x) (y)
exclusively.f(x, y)
but this proposal has encouraged me to adoptf(x)(y)
orf (x) (y)
.f(x, y)
but find the arguments for dropping Ramda-style currying compelling. I would adoptf(x)(y)
orf (x) (y)
if necessary.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.
The text was updated successfully, but these errors were encountered: