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

Closed
davidchambers opened this Issue Sep 1, 2017 · 55 comments

Comments

Projects
None yet
@davidchambers
Member

davidchambers commented Sep 1, 2017

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.

@JAForbes

This comment has been minimized.

Member

JAForbes commented Sep 1, 2017

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.

@kurtmilam

This comment has been minimized.

kurtmilam commented Sep 1, 2017

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.

@CrossEye

This comment has been minimized.

CrossEye commented Sep 1, 2017

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.

@miwillhite

This comment has been minimized.

miwillhite commented Sep 1, 2017

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.

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 1, 2017

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.

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 1, 2017

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?

@miwillhite

This comment has been minimized.

miwillhite commented Sep 1, 2017

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

This comment has been minimized.

CrossEye commented Sep 1, 2017

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.

@CrossEye

This comment has been minimized.

CrossEye commented Sep 1, 2017

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

@miwillhite

This comment has been minimized.

miwillhite commented Sep 1, 2017

@CrossEye you are correct…I took some liberties in my response to play around with what it might look like in the "ideal" state ;)

@svozza

This comment has been minimized.

Member

svozza commented Sep 1, 2017

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.

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 1, 2017

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

This comment has been minimized.

Member

davidchambers commented Sep 1, 2017

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.

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 1, 2017

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?

@kurtmilam

This comment has been minimized.

kurtmilam commented Sep 2, 2017

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

This comment has been minimized.

Member

svozza commented Sep 2, 2017

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.

@tycho01

This comment has been minimized.

tycho01 commented Sep 2, 2017

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

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 2, 2017

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.

@svozza

This comment has been minimized.

Member

svozza commented Sep 2, 2017

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.

@dakom

This comment has been minimized.

dakom commented Sep 3, 2017

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.

@dakom

This comment has been minimized.

dakom commented Sep 3, 2017

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.

@dakom

This comment has been minimized.

dakom commented Sep 3, 2017

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)

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 3, 2017

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

@miangraham

This comment has been minimized.

Collaborator

miangraham commented Sep 3, 2017

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!

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 3, 2017

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.

@ScottFreeCode

This comment has been minimized.

ScottFreeCode commented Sep 4, 2017

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.

@tycho01

This comment has been minimized.

tycho01 commented Sep 4, 2017

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

@ScottFreeCode

This comment has been minimized.

ScottFreeCode commented Sep 4, 2017

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!

@syaiful6

This comment has been minimized.

syaiful6 commented Sep 4, 2017

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.

@joshburgess

This comment has been minimized.

joshburgess commented Sep 8, 2017

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

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 8, 2017

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.

@joshburgess

This comment has been minimized.

joshburgess commented Sep 8, 2017

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

@tycho01

This comment has been minimized.

tycho01 commented Sep 8, 2017

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.

@davidchambers

This comment has been minimized.

Member

davidchambers commented Sep 8, 2017

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

@kurtmilam

This comment has been minimized.

kurtmilam commented Sep 8, 2017

@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 )
@dardub

This comment has been minimized.

dardub commented Oct 17, 2017

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.

@dakom

This comment has been minimized.

dakom commented Oct 17, 2017

@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

@masaeedu

This comment has been minimized.

Member

masaeedu commented Nov 22, 2017

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.

@Avaq

This comment has been minimized.

Member

Avaq commented Dec 14, 2017

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.

@davidchambers

This comment has been minimized.

Member

davidchambers commented Dec 17, 2017

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.

@dakom

This comment has been minimized.

dakom commented Dec 18, 2017

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

This comment has been minimized.

tycho01 commented Dec 18, 2017

@davidchambers

This comment has been minimized.

Member

davidchambers commented Mar 9, 2018

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

This comment has been minimized.

onetom commented Oct 26, 2018

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.

@onetom

This comment has been minimized.

onetom commented Oct 26, 2018

I just noticed https://github.com/joelnet/eslint-config-mojiscript which seems to achieve this style with eslint.

@davidchambers

This comment has been minimized.

Member

davidchambers commented Oct 26, 2018

If you find a solution that works for you, @onetom, please share it here. We could then add it to the readme and website.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment