sanctuary-js / sanctuary

:see_no_evil: Refuge from unsafe JavaScript

Home Page:https://sanctuary.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

readable code without Ramda-style currying

davidchambers opened this issue · comments

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) and f(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 than f(x)(y). Teams would not need to choose one style or the other (although there may still be f(x) versus f (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.

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 (ifte is my version of Ramda's ifElse, in this case):

const getValue =
  ifte( isConstructed )
      ( prop( 'value' ) )
      ( id )

As a follow up, here's how your def example looks in my style:

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

Keeping Fantasy-land compliance would seem to necessitate being able to support certain binary functions, for Foldable, Bifunctor, and Profunctor, and perhaps others.

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 S.mult as an argument to S.reduce, and you're worried that you need an uncurried binary function because fantasy-land/reduce methods expect such a function. In fact, S.reduce requires a curried function, so there's no mismatch.

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 R.reduce takes an uncurried binary function one would not be able to provide R.multiply as an argument to R.reduce unless we were to change the type of R.reduce at the same time as we removed Ramda-style currying.

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:

Sometimes it is better to favour consistent legacy technology over fragmentation.

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:

  • Sanctuary's run-time type checking would highlight call sites with multiple arguments; and
  • we could write a source-to-source transpiler for replacing occurrences of S.<name>(x, y) with S.<name>(x)(y) or S.<name> (x) (y) based on the user's preference.

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'm confused by this. Sanctuary functions of arity 2 or 3 would be affected by the change unless you're already using the f(x)(y) style. Could you clarify this comment?

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);

As often seems to be the case, decisions in the Ramda world involve more complicating factors than decisions in the Sanctuary world.

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 Foldable f a :: (b -> a -> b) -> b -> f a -> b to Fantasy-land's Foldable f => f a ~> ((b, a) -> b, b) -> b. For b -> a -> b and ((b, a) -> b would then require some translation before delegating, and that sounds confusing.

@miwillhite:

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 sanctuary-def's currying code but that seems to be around making our lives easier, i.e., the maintainers, rather than our users.

But I would worry about, say, delegating a Foldable f a :: (b -> a -> b) -> b -> f a -> b to Fantasy-land's Foldable f => f a ~> ((b, a) -> b, b) -> b. For b -> a -> b and ((b, a) -> b would then require some translation before delegating, and that sounds confusing.

We do need to transform b -> a -> b into (b, a) -> b, but this is straightforward:

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.reduce simply forwards its arguments to the underlying method, but I don't consider the use of uncurry2 surprising. I see the fantasy-land/-prefixed methods as the messy machinery with which we can build a beautiful API. The type of S.chainRec, for example, differs significantly from the type of the underlying function:

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.

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.

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 S.concat(x, y) were to become a type error.

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 f (x) (y) and a mental trick to help me sleep at night whereas yours may simply involve f(x)(y). If every member of the community could find some way to be happy in a comma-free world I'd be happy to make the breaking change.

Stefano, could a mutiny be prevented if S.concat(x, y) were to become S.concat(x)(y) and there were a command-line script to automate the translation?

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 curry function like Ramda's, although @tycho01 is actively working to simplify things in this regard.

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;
    }
}

To be clear, I'm not suggesting that we dictate how anyone format their code

Sorry, I was being hyperbolic but I find the )( way so ugly that I think I'd be compelled to use some variant of the proposed style just to keep the code readable. I don't feel like I should have to make that choice in order to use Sanctuary.

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.
In fact, in your manually curried example I think the type would just be inferred already:

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 fn(a, b)(c), separating only the values, which may look funny but meant values were clearly identified while remaining composition-proof (unlike fn(a, b, c)).

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.

I wasn't clear. I'm proposing a script to be run once per project to update files in place:

$ sanctuary-migrate index.js lib/**/*.js

It would save each of us from performing the :%s/, /)(/gc replacements manually.

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 (a)(b) or (a) (b) is more accurate in terms of what currying should look like, then that's what it should be - old habits be damned! :) Learning to adapt a clearer approach will pay off in the long run. Those who don't put in the investment it takes to change this habit are unlikely to put in the investment it takes to change plenty of other habits relevant to this space (avoiding mutation, passing monads around, writing things more declaratively, etc.). Maybe that can't apply to everything (e.g. if you could enforce writing type signatures for every function, which I'd actually appreciate), but I think for something as fundamental as this it's a fair ask.

In other words, I think this:

the library's initial appeal

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 length as 1, in accordance with its type signature. One minor unanticipated benefit of this change is that we'd no longer be forced to impose an arbitrary limit on the number of arguments a function may accept.

It's worth having a look at the diff to see the f(x)(y) style in action (def itself is defined via def, so requires its arguments one at a time).

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 (a)(b)(c) and (a, b, c) instead of every possible paren permutation plus placeholders. Two versions no matter how many arguments. Comma lovers could have their commas. Implementation of the non-curried version could be simply calling uncurry on the curried implementation. Even if this isn't appealing for the library itself, users could always do the same thing.

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!

Shape the types as best you can, then throw up your hands and wait for improvements.

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.

I'm new to the library so grain of salt!

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:

  • f(x, y, z) makes sense for non-curried functions (or (f x y z) if you feel the need to encode functions as linked lists of data, but I digress)
  • f(x)(y)(z) is hard to read even though it makes sense for curried functions
  • f (x) (y) (z) looks a little odd, but makes sense for curried functions and is easy to read (much like how (f x y z) is bizarre until you understand what it really means -- and how things like (+ 1 (* 2 3) 4) nest uniformly instead of requiring tons of operators chained together -- and then it actually becomes surprisingly clear)

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 f(x, y) to mean "supply only the first two arguments to f(x, y, z)" is a lot less obvious than using f(x)(y) (or a more readably spaced variation) to mean that, or even than using f(x) to mean "supply only the first argument to f(x, y)". So the Ramda-style currying doesn't make a lot of sense to me to begin with -- in hindsight, the mixed way of thinking it encouraged exacerbated my initial wariness of currying because "how will I know whether this code is calling a regular function that returns a value or supplying the initial arguments to a curried function?" (now my answer would be "if it passes the arguments one at a time, it's obviously curried, if it passes multiple arguments, it should not be curried, and if it's only passing one argument you should be able to tell from whether the return value is named like an operation or is passed into something expecting a callback.")

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.

@ScottFreeCode:

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.

Function composition, R.pipe and its variants. Don't need to name a variable per operation, and you get to read in a nicer order than h(g(f(x))). Lodash-FP exists but is an afterthought in terms of documentation.

Ha, can't believe I forgot about compose/pipe and things like add not being widely available outside of Ramda/Sanctuary. I knew I'd learned multiple great ideas from Ramda, but wow, the things I take for granted after I get used to them -- even if I'm not even using them day to day...

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 f(x, y) seem more natural rather than f (x) (y). If we want to drop Ramda style currying function, we maybe need to introduce partial function, so it's become Clojure-like.

The great things about Ramda's style currying function is it's readable, and f(x, y) seem more natural rather than f (x) (y).

I agree, @syaiful6. My poll response was 😕 for this reason. I'm starting to think, though, that the sacrifices we're making to support two styles outweigh the readability benefits of doing so.

If we want to drop Ramda style currying function, we maybe need to introduce partial function, so it's become Clojure-like.

If I'm understanding you correctly you're suggesting this change:

  • f(x)(y)(z)
  • f(x, y)(z)
  • f(x)(y, z)
  • f(x, y, z)

I'm suggesting this change:

  • f(x)(y)(z)
  • f(x, y)(z)
  • f(x)(y, z)
  • f(x, y, z)

I can't imagine ever giving up f(x)(y)(z) as it is central to the Sanctuary experience (and to the Ramda experience). S.partial(S.prepend, [x]) is awkward when compared with S.prepend(x).

I'm proposing that we take a step closer to Haskell rather than to Clojure. :)

@dakom:

If (a)(b) or (a) (b) is more accurate in terms of what currying should look like...

I think that's a fundamental "if". Undoubtedly, that's closer to what Schönfinkel and Curry first noted: that functions of multiple parameters could always be stated in terms of nested functions of single ones. (Mathematically, there's another way to accomplish this, with a domain simply being the crossproduct of each parameter's domain. This can be useful in programming as well, as evidenced by Function.prototype.apply, but it's very different from our currying.)

Ramda's version of currying grew out of the same spirit as the rest of the early parts of the library: it was a learning experiment, focused on the question of how we could take the techniques that make functional programming so useful and port them into reasonably familiar Javascript. My go-to example is often this one, expressed here in Haskell:

myTotal = foldl add 0 myLineItemTotals

which leads to this function:

invoiceTotal = foldr add 0

which, in turn, has the obvious abstraction

sum ::  Num a => [a] -> a
sum = foldr add 0

Porting this notion to Javascript had to us at the time an obvious version:

const sum = reduce(add, 0)

We weren't really porting over the specific Haskell syntax, just the notion of partially applying a function to get a new one with fewer parameters. We did consider pure currying, but felt, especially before ES6 became so common, that this was too far from common JS practice:

const f = expr(a, b, c)
curry(f)(a) ~> function(b) {return function(c) {return expr(a, b, c)}}

Whereas this was more easily understandable:

const f = expr(a, b, c)
curry(f)(a) ~> function(b, c) {expr(a, b, c)}

Ramda opted for the flexibility that allowed either usage.

With ES6 arrow functions this no longer seems as big an issue:

const f = expr(a, b, c)
curry(f)(a) ~> b => c => expr(a, b, c)

and its no longer clear if the additional flexibility of Ramda's version is worth either the cognitive burden or the implementation complexity. And if we were to pick one, the version closer to the Schönfinkel/Curry ideas would surely carry the day.

That's why I'm intrigued with the proposal for Sanctuary. I don't share David's love for the comma, although I am certainly used to it. There is a lot to be said for the clear simplicity of pure currying. And the suggested spacing of

f (a) (b) (c)

makes it feel much more like the Haskell equivalent.

I'm not sure I love it, but I do think it very much worth considering.

@miangraham:

If moving to simple currying, you might also consider keeping completely non-curried versions around. [ ... ] The net result would be (a)(b)(cNo opinion on whether it's the same function or not, but dealing with a single polyadic interface is pretty trivial.) and (a, b, c) instead of every possible paren permutation plus placeholders.

👎

I see no reason to keep (a, b, c) and not (a) (b, c) or (a, b) (c). I think it's either go whole-hog, and turn it into strictly (a) (b) (c) or don't bother.

@CrossEye - thanks, while I can't (yet) understand the Haskell examples entirely, the clear breakdown is awesome - it's great to understand the history here with greater clarity. Diving into all this before ES6 must have been a blast ;)

@CrossEye

I see no reason to keep (a, b, c) and not (a) (b, c) or (a, b) (c).

To clarify what I already wrote: The implementation (library complexity) and the interface (wrapper/user complexity) are significantly simpler when you stop supporting every possible combination of parenthesis and commas. Only supporting (a)(b)(c) means 1 version of the function no matter how many arguments. This is good. Supporting (a)(b)(c) and (a, b, c) is 2 versions no matter how many arguments. Not as good but not a big deal. Supporting all combinations is 2^(n-1) in the number of arguments (every space between arguments is either , or )().

That's a big difference, especially when you're currying functions with 5+ parameters. This is at least one reason why Ramda needs code generation to express its curried functions as TypeScript interfaces. Going from 2^(n-1) to 2 here would still be an enormous win.

I didn't want to weigh in on this specifically but so my motives aren't mistaken: FWIW I'm also completely in support of purely unary currying. I love Haskell's approach and think widespread arrow functions now make this work pretty well in JS. Pretty sure we agree on this.

My suggestion was spitballing to see if a simple addition could ease the transition. As I mentioned above, it's not a big deal--building the second interface is just a call to uncurry(). If the library doesn't want it and users do, adding it is trivial.

As for TS, we'll get past the codegen, hopefully sooner rather than later. I think the only relevant issue for which I don't have a PR up yet (be it WIP) is microsoft/TypeScript#17471.

Going from 2^(n-1) to 2 here would still be an enormous win.

I agree.

Another reason we might decide to support (a)(b)(c) and (a, b, c) in some fashion is that the underlying implementations are uncurried functions. def takes an uncurried function and returns a curried function which performs type checking. We may decide there's value in exposing the wrapped function. After defining const add = def('add')({})([…])(_add);, for example, add.impl could be a reference to _add. Sanctuary could then potentially expose this as S._add for convenience. I'd prefer to avoid even this degree of complexity, but it's worthy of consideration.

gotta run - but another tiny data point, one of the earlier pains I experienced as a newbie is reading type signatures which looked very foreign to me. It might seem like a small thing but the consistency of seeing a -> b -> c in the docs, writing code as a => b => c, and using Sanctuary as (a) (b) (c) helps it all click a little easier - one less hurdle to overcome for understanding how things fit together

ramda/ramda#2272 shows that Ramda-style currying, which seems natural to those of us who've "grown up" using it, is not natural to everyone. This is one more data point in favour of simplification.

+1 for simplifying to only one argument at a time for each function call. I've found myself always using f(a)(b)(c) when using Ramda and Sanctuary even though it's not enforced, because:

  1. It's consistent with how currying works in other languages (Haskell, PureScript, etc.)

  2. It's simpler to limit how the curried functions can be called to just one way. When you allow multiple ways, there will inevitably be members of a team who use g(b, c) and others who use g(b)(c), and you'll end up with apps with mixed, inconsistent codebases. While not the biggest problem in the world, it's definitely simpler to have all the code read the same way.

I think I'm with @CrossEye in that I'm used to & don't have any issues with using commas, but don't share the love for them.

Personally, even though some people find the )( style aesthetically displeasing, I think it's valuable in that it immediately clues the reader into the fact that they're dealing with curried functions precisely because it looks different from standard, non-curried/non-functional JavaScript. I think that's helpful when working with teammates who are new to FP & currying in JS, because they might not realize where the g in g(b, c) comes from or that it is a curried function itself otherwise. So, I see it as a helpful visual cue.

As for the extra spacing of f (a) (b), I agree that it does look slightly nicer and more Haskell-like, but I don't have strong opinions about it vs f(a)(b). They're both doing the same thing, after all. Although the non-spaced version does looks slightly more noisey, I just see it as an artifact of the language. JS requires parens for function calls, unlike ML style languages. So, it has parens. ¯\_(ツ)_/¯

Also, there might be trouble in getting the spaced convention adopted, because there are a lot of popular linting configs that disallow spaces between function names and parens when calling functions. I believe even StandardJS has this rule, despite requiring spaces between them in function definitions.

Thanks for your feedback, @joshburgess.

there might be trouble in getting the spaced convention adopted

My intention is not to get everyone to adopt one particular set of formatting rules. The impetus for creating this thread was the approach that occurred to me that I thought might make strict currying more palatable to some people (including me). Those already treating Sanctuary functions as if they were strictly curried are already aboard the train and will be unaffected if we switch to strict currying.

@davidchambers Personally, although it's foreign to idiomatic JS, I like the spaced style you presented. I would prefer to use that if given the choice.

I always thought it was slightly odd for StandardJS (the linting config I use for plain JS) to require spaces in definitions and require no spaces in calls. Although, I suppose people could just add an override to turn off that conflicting rule. Many already do that to allow trailing commas, etc. anyway.

So with the proposed drop of Ramda currying, function application would switch out a few commas for brackets, as demonstrated.

The effects on say function composition would be a bit bigger, fwiw. Given a previously Ramda-curried binary function, let's call it f1, an expression like f2(f1(a, b)) could previously have been rewritten as pipe(f1, f2)(a, b). With simple currying, one could still write pipe(f1(a), f2)(b) instead. pipe(f1, f2)(a)(b) on the other hand, would no longer be equivalent.

I was thinking the change might also affect places where previously we'd be expecting multi-ary callback/whatever functions, but so far no concrete examples have come to mind.

Given a previously Ramda-curried binary function, let's call it f1, an expression like f2(f1(a, b)) could previously have been rewritten as pipe(f1, f2)(a, b). With simple currying, one could still write pipe(f1(a), f2)(b) instead. pipe(f1, f2)(a)(b) on the other hand, would no longer be equivalent.

This is a problem for Ramda but not for Sanctuary. S.pipe takes an array of unary functions. ;)

@tycho01 I think your pipe example is a good example of the case for Ramda's o:

Written with o and simple currying (should work, haven't tested):

o( o( f2 ) )
 ( f1 )
 ( a )
 ( b )

Regarding R.pipe, I usually swap it out for a binary version in my projects:

const pipe = R.apply( R.pipe )
// this:
R.pipe( f1, f2 )( data )
// becomes this:
pipe( [ f1, f2 ] )( data )
// or just use Sanctuary's pipe, which works the same way:
S.pipe( [ f1, f2 ] )( data )

Honestly from my viewpoint (as a beginner), Sanctuary isn't for beginners into functional javascript anyway. Just going to https://sanctuary.js.org/ to learn more about the project scares me away from using it.

I think Ramda is more well known at outside fp community and serves as a good starting point for beginners. I've only looked into Sanctuary as a means for handling option types.

What I'm trying to say is there any data to support that Sanctuary is a destination for newcomers that would be scared away by the suggested change?

Edit: I could be misinterpreting the meaning of newcomers however.

@dardub anecdotally, for me: a => b=> c => {...} is straightforward and easier to understand than curry((a,b,c) => {...}).

It does make it a bit harder to port old habits/code, but it's a native language thing and easier to grasp without having to read up on "currying" and other FP-specific things.

Personally, I don't think this issue is the obstacle for newcomers. ymmv

A hesitant +1 for this. The thing is, sometimes being forced to curry a long sequence of arguments ends up revealing a point-free representation using standard combinators, which pays for itself by deleting (a, b, c, ...) => at the beginning of the function.

I just changed my vote from 😕 to 👍

I've adopted this style of function application in all of my experimental work and I've come to appreciate the reduced mental load a lot.

I'm becoming increasingly eager to use this style across an entire project, and I think that in my case, the kind of projects suitable for using this style greatly overlap with the kind of projects suited for using Sanctuary.

Aldwin, I too have just changed my vote from 😕 to 👍. I've come to appreciate having fewer commas in my code. I find f ([a, b]) ([c, d]) clearer than f([a, b], [c, d]). Only using , in [e, e, e] and {k: v, k: v, k: v} allows me to infer context: if I see 1, 2, 3 I know these are array elements rather than arguments even without seeing the surrounding square brackets.

Ramda-style currying is on the way out: sanctuary-js/sanctuary-def#179. Sanctuary functions will be “simply” curried from v0.15.0 onwards.

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));

Tasks:

  • Remove placeholder (S.__)
  • Remove S.A
  • Remove flipped operator functions (S.lt_, S.lte_, S.gt_, S.gte_, S.sub_, S.div_, S.pow_)
  • Change types of operator functions from _ -> (_ -> _) to _ -> _ -> _
  • Reformat doctests
  • Upgrade sanctuary-def dependency

Is there a https://prettier.io configuration for this function application spacing style?
I think it goes against adoption if we help people to configure their automatic code formatters accordingly.
I'm just using the built-in IntelliJ settings, btw, but I would consider switching to prettier.

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 eslint-config-mojiscript allows for automatic reformatting of the parentheses and some other helpful linting.

npm i -S eslint-config-mojiscript

.eslintrc.js

{ 
  ...,
  extends: 'mojiscript',
  ...
}

UPDATE:

The eslint rule that allows for this to be automatically formatted is func-call-spacing. Should be as easy as adding:

.eslintrc.js

{
  ...,
  rules: {
    'func-call-spacing': ['error', 'always', { allowNewlines: true }]
  },
  ...
}

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 '@typescript-eslint/func-call-spacing' instead of 'func-call-spacing'.

Also, WebStorm supports an auto-fix through the built-in options Preferences > Editor > Code Style > [Typescript/Javascript] > Spacing > Before Parentheses > Function Call Parentheses.

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 prettier, eslint and VSCode.