Currying makes positional only parameters look cooler and fancier. It's a trap. Labeled arguments is the way to go for 99% of all parameters. Accidental currying is horrible.
If you look at how people actually write PureScript, which is much like Haskell, but with anonymous records, you can see that they're not actually used 99% of the time.
Somewhat related - be wary of implicit parameter passing in function pipelines. For example, Try ['10', '10', '10'].map(Number.parseInt) in your browser console. What's actually being called is:
Unfortunately typing doesn't fix this. The 2nd callback function argument of Array.map is (index:number), and the 2nd argument of Number.parseInt is (radix:number).
If there were typing, there would be no reason to not use newtypes[0] for radix and index. These values should only be explicitly unwrapped to plain numbers.
It would look like this
newtype Radix = Radix { radixToWord :: Word }
newtype Index = Index { indexToWord :: Word }
One of my favorite features in OCaml is labeled arguments. You get a similar flavor of partial application, but without the strict argument order requirement (most of the time -- higher order functions are strict about labeled-function arguments).
The technique you describe requires us to predict what data should be partially applied (before actually using it), and what data should be applied and used immediately. In contrast, currying/partial application doesn't require this decision from us -- for this reason I consider the functional approach more flexible than the object-oriented one.
In addition to that, I don't always find the constructor/method approach more readable. It _can_ be readable, if you design your API with care; however, it is common to group related data into records in the FP world as well, which mimicks OOP constructors, in a sense.
Yes, like many refactorings, rewriting a function as an immutable class with a method is not always a win. It depends on how you want to group a function’s parameters, whether that grouping has an intuitive name, and whether you might want to write other methods that take the same initial parameters. Sometimes grouping parameters using a record type is better, though you don’t get the nice syntax.
If currying isn’t built-in, you can emulate it. In TypeScript you need to do it explicitly, which means anticipating that it will be used, and whenever I try that, I later decide that it’s needlessly obscure and rewrite the code some other way.
Even with built-in currying, you still need to anticipate usage by changing the order of the parameters, based on which ones you expect the callers to have first. It’s less general than writing an inline function with exactly the parameters you need. TypeScript has nice syntax for defining tiny, one-off functions and I think that’s better than having currying.
One side-effect of having currying in a language is that it discourages naming things, and I think that’s often bad for readability. A chain of method calls gives you a lot of names to look up and a place to put documentation.
No, not exactly. And partial application of any function is nowadays possible using anonymous functions to wrap the original one, no need for constructors and special methods.
What currying in Haskell and any other ML does, is the following as Javascript. Let's say we have a function with four parameters `func(a, b, c, d) {return a + b + c + d;}`, then the curried version is the following:
func = (a) => {
return (b) => {
return (c) => {
return (d) => a + b + c + d;
}
}
}
You're assuming that the poster doesn't understand what currying is and you're jumping in with a correction which is kinda rude.
Instead of assuming you know better and jumping in with a correction, could you reread that post and assume that the person knows what they're talking about but isn't communicating exactly the way you would have communicated it? I.e., read with the intent of understanding intent, rather than read with the intent of correcting?
It is clear that that poster does not understand what currying is, as defined in the first line of the article and easily found online; also, downthread [1], nor do you. Currying is when you turn a function of multiple parameters into nested functions of single parameters. Partially applying a curried function is not called currying.
Though if enough people share the confusion, I guess it becomes an alternative definition. Language evolves.
Also, in any case, a constructed object is nothing like a partially applied function, so the parallel drawn is not useful. It seems entirely reasonable in this case to assume that the poster does not have sufficient experience or knowledge.
I can see how someone might think I don’t know the difference, but it’s rather that I don’t think the details are important. They are both ways of partially applying a function. I wanted to demystify currying and show the equivalence, but I was too brief about it.
A method is a function with convenient syntax. Some of its arguments come from the “this” parameter. If the object is immutable and the method has no side-effects, it’s a pure function.
Converting a function to an immutable class with a method is a mechanical refactoring that I sometimes do, when the class has an intuitive name. The code does the same thing.
We can say that when a function is curried (i.e. prepared or converted to curried form) then it supports partial application without any further transformation. In fact it happens naturally whenever the function is invoked.
E.g. if we have (lambda (x y z) (+ x y z)) converted into the curried form (lambda (x) (lambda (y) (lambda (z) (+ x y z)))), then while we do not yet have partial application, we can then partially apply just by passing arguments.
That is to say, we are partially applying in the abstract sense; if we pretend we still have a (lambda (x y z) ...) in our abstract semantics, then when we pass a value for x to the curried function, we then have effectively partially applied this abstract function, resulting in something that needs just the (y z) arguments now.
Languages that support implicit partial application erase the difference between f x y and (f x) y. That is to say, the programmer doesn't have to know whether f x y is a call two a function f with two arguments, or whether there is a partial application (f x) which binds the x argument, followed by an application of that resulting function to argument y.
Now if the language substrate is based on one-argument functions, under the hood, so that all functions with two or more arguments are implicitly transformed into one argument functions by currying, then partial application is "free", so to speak. The syntax may let you write f x y to call a function of two arguments, but the implementation is actually doing (f x) y.
Thus currying can support partial application in a similar way to how transformation of a program to CPS (continuation passing style) supports continuations. Partial application is "free" in a curried program similarly to how continuations are "free" in a CPSed program. Under CPS, you already have the return continuation as a hidden argument, so call/cc does nothing more than reveal what is hidden. Under currying, you already have a lambda that takes just one argument, and gives you a lambda that takes the next one.
I think when calling a curried function it isn't easy to differentiate between a simple function application and a partial (function) application. I would make the assumption that as long as a higher-order function is called, then what we're actually doing is a function transformation and thus a partial application - when we applied all arguments except the last one, then finally, when the last argument of a first-order function (= the partially applied higher-order function) is applied, then this function can not be transformed anymore and is simply just evaluated. I'm sure there's also a difference in lazy evaluated languages and others in there, but this is beyond my competence.
> Also, in any case, a constructed object is nothing like a partially applied function, so the parallel drawn is not useful.
Are you being serious? The fact that closures and objects are dual is a frequently made observation. It’s not something the poster invented and it’s not (usually) controversial.
Are you? Closures were not mentioned and are an implementation detail. In any case, an object and a bundle of closures being equivalent still has nothing to do with OP's talk of method calls being "easier to read and work[ing] better with autocomplete". Just read the original message upthread and try to make any useful meaning of it (especially in the context of turning add(1,2) into add 1 2).
> but isn't communicating exactly the way you would have communicated it?
My answers are almost always less intented for the person who's post I'm responding to, but more for a general audience. As others have already posted the part about "currying isn't partial application", and I mainly wanted to add a (hopefully) understanable example.
And I would say that taking the OP's post as if he doesn't exactly know, what currying means in comparison to partia application _is_ the more favourable interpretation of their post.
> My answers are almost always less intented for the person who's post I'm responding to, but more for a general audience
Same. I don't expect you're going to admit your mistake, but I'm pointing it out because the general audience is the sort of audience who is prone to this mistake (which I only know because I myself have been corrected for making this mistake).
> As others have already posted the part about "currying isn't partial application", and I mainly wanted to add a (hopefully) understanable example.
And in doing so, you're making the same mistake as those other posters, by assuming skybrian doesn't know what currying and partial application are. You're not doing your audience any favors by painting skybrian's post as if he said something wrong.
> And I would say that taking the OP's post as if he doesn't exactly know, what currying means in comparison to partia application _is_ the more favourable interpretation of their post.
But instead of explaining or mentioning this, you both went into a rant about intentions and motivations, which completely bored me, and informed me of nothing, as a member of the general audience.
Are you saying this to be helpful, or just to show you're smarter? If you're saying this to be helpful, you should probably explain more.
Currying is a subset of partial application, so I suspect the person you're responding to is just being a bit loose with their wording, as opposed to not understanding the topic.
I don't read it that way, no. Currying is a process to transform a function that takes multiple arguments into higher order functions that take one argument at a time to enable partial application (as written in the article). The partial application of a function is a different transformation of a function.
Some languages provide partial application without currying a function - JavaScript has bind, Python has functools.partial etc.
I thought it was somewhat the reverse — that the original motivation was to simulate multi-arity functions in a setting that only allows unary functions.
I don't really understand the distinction between "multi-arity" and "unary" functions. In my mental model, all functions are unary, it's just that in "traditional", non-FP languages it's more common to pack arguments into tuples. That is `(a, b, c) => ...` in JS is a unary function which takes a tuple of 3 items. But the thing is, FP has tuples too (and in Haskell, with pretty much the same syntax), it's just that most of the time arguments aren't packed into tuples.
So could you elaborate to me where the distinction lies here and why is there a need to "simulate" things?
Say you are dealing with a callback mechanism that takes a function and (only) one value to pass:
function c(f, v) { ... f(v) ... }
But what you actually need this callback mechanism to call is a function that takes two arguments. No default values.
function f2(a, b) { ... }
There's no way in js that c is going to call f2 and set its b parameter to something. You can't set v to be a tuple so b will be filled:
c(f2, "value") => b will be undefined.
c(f2, [1, 2]) => a will be set to [1, 2].
you need to wrap f2 for this when calling c:
function wrapf2(b) { return f2("fixed a", b) }
Or you can make c deconstruct v, or use apply:
function c(f, v) { ... f(...v) ... }
function c(f, v) { ... f.apply(null, v) ... }
but then v always needs to be an array:
c(f, [1])
c(f, [1, 2])
c(f, 1) // fails
This is why in Javascript, multi-arity is different from unary with tuples. JS doesn't have tuples as a primitive / first-class type anyway. In some math notations we don't make the difference, but in most programming languages there's a distinction. Your model where functions are always unary but can take tuples, and (v) is the same as v, is correct (and useful when the distinction doesn't matter and is just annoying to deal with), but doesn't match the way many programming languages actually work. I believe even in most functional programming languages, (v) is different from v, you also said it.
In assembly, different parameters are in fully separate registers or stack entries. Interestingly, you could imagine representing tuples as C structs (that can have only one member) and always pass structs, and passing a one-member struct will actually have the exact same effect as passing a value of a primitive type.
Okay, I understand what you're saying, and you're right, but you're missing my point and doubling down on the same mistake again.
Language is inherently imprecise and communication is never perfect. When people say something, it's rude to assume they are wrong or don't understand something because they don't say it exactly the way you would have said it. This is extremely common in technical communication and it's extremely counterproductive.
I'm suggesting that you could be a better reader by trying to understand what people are trying to say even if they don't communicate it exactly the way you want them to.
For example, no one in this comment chain is confused about what currying or partial application are, so it's a bit rude that you've assumed people are confused because you didn't take the time to figure out what people were trying to communicate. Instead, because people didn't say things exactly the way you would have said it, you jumped in with corrections.
If you take a step back and resist the urge to correct people, can you understand how "Currying is a subset of partial application" is true?
It does not appear to me that skybrian is first learning about currying and partial application, and I'm not first learning about currying or partial application in this thread either.
Your play at humility that you didn't understand currying and partial application in the past kinda falls flat if you then go on to less-humbly assume that everyone else in the conversation is stuck where you once were.
> ...can you understand how "Currying is a subset of partial application" is true?
It isn't true and i can understand why someone would make that statement. Currying and partial application both come from a functional mindset that treats functions as data, and like data also functions can be transformed.
Currying and partial application both satisfy a transformation depending on a desired context:
- Currying "widens" a function "up" (as in you don't need to know all arguments beforehand in the same context anymore)
- Partial application "narrows" a function "down" (as in arguments are contained within the context of the partially applied function)
"Currying is a subset of partial application" is true in the same sense as if "subtraction is a subset of addition". Sure, you can subtract negative values to have addition and add negative values to have subtraction, but that can't be your point, is it?
> If you take a step back and resist the urge to correct people...
> It isn't true and i can understand why someone would make that statement
It is true, and you are smart enough to understand it if you stop assuming you're the only one who understands the topic. I'm specifically sticking with the wording "Currying is a subset of partial application" to make the point that you can understand what someone is trying to say even if they don't say it exactly the way you would like them to say it.
> - Currying "widens" a function "up" (as in you don't need to know all arguments beforehand in the same context anymore)
> - Partial application "narrows" a function "down" (as in arguments are contained within the context of the partially applied function)
So you're saying that I'm "widening" the function "up" when I do:
increment = add 1
invert = div 1
...? But I'm "narrowing" the function "down" when I do:
increment = (\x -> add 1 x)
halve = (\x -> div x 2)
...?
When I curry `add` above, isn't the argument 1 contained within the context of the partially applied function?
When I partially apply `div` above, don't I no longer have to know all the arguments (i.e. I no longer need to know the denominator 2)?
It sure seems like this up/down wide/narrow wording you're fixated on isn't a particularly better descriptions of what's going on with currying and partial application.
It mean, maybe it's just my font, but the partial applications are a bit wider on my screen. ;P
> "Currying is a subset of partial application" is true in the same sense as if "subtraction is a subset of addition". Sure, you can subtract negative values to have addition and add negative values to have subtraction, but that can't be your point, is it?
I'm not sure I understand that analogy to say it's my point.
Are you saying you would behave condescendingly toward someone who said "subtraction is a subset of addition" too?
> Please keep it on topic.
The topic skybrian started was derailed when you decided to "correct" him because you didn't make any effort to understand what he was trying to say. Your behavior is the topic now because your behavior became a problem.
Neither of your examples does curry a function. The first one partially applies it (the Haskell way), the second one wraps the partial application in a redundant lambda (Haskell again). I think that's where the confusion is, let me also try to use the Haskell syntax...
This is an add function with two arguments:
add :: (Int, Int) -> Int
add (x, y) = x + y
At the moment it can not be partially applied, so let's curry it:
-- addCurr :: Int -> Int -> Int
addCurr = curry add
Now let's partially apply it for an increment function:
-- increment :: Int -> Int
increment = addCurr 1
> When I curry `add` above, isn't the argument 1 contained within the context of the partially applied function?
You didn't curry it, you partially applied it. This was possible, because your function add was already curried.
Coming back to this having spent a wonderful day outdoors.
Okay, writing this in Python because a) it's been a while since I wrote Haskell, b) it will let other readers read the code more easily, and c) Python clearly denotes what a partial is by having a `partial` function:
from inspect import signature
from functools import partial
def curry(f, parameters=None):
if parameters is None:
parameters = list(signature(f).parameters.keys())
if len(parameters) == 0:
return f
head = parameters[0]
tail = parameters[1:]
# I draw your attention to this line
return lambda p: curry(partial(f, **{head: p}), tail)
def divide(a, b, c):
return a / b / c
curried_divide = curry(divide)
# prints "3"
print(curried_divide(30)(5)(2)())
So... curry can be implemented in terms of partial applications, no?
Yes. Why not? Would you be confused about what someone meant if they said that statement?
My critique of your original post is that you're treating English like it's a computer language where words mean very specific things. But the reality is that English is a cobbled-together bunch of words with imprecise meanings that arise from common usage. Let me reiterate: usage determines meaning, not the other way around.
You're approaching this conversation with your own preconceived notions about what the words mean and trying to force your preconceived notions onto everyone else instead of trying to understand what the other person means. It's a rude way to treat people trying to communicate with you.
I mean, it's clear that you understand the idea because you're coming up with examples that show that you understand it. Communication was successful. There's no problem with the communication. The problem is that you decided, after communication was successful, to force your preferred usage of the words on other people.
This is an extremely common interpersonal mistake that technical people make because unlike English, computer languages have very specific, objective meanings. But expecting that same objective specificity from humans speaking English isn't realistic, kind, or polite. It's one of the main reasons technical people are widely perceived as socially inept. Behaving like this hurts our careers, friendships, and romantic relationships. Doing this on HN is just a minor annoyance to everyone else, but if you do this in your life as a whole, the person you're harming most is yourself, because you're making it a pain in the ass to communicate with you.
I'm asking you to be a better reader and listener.
And look, maybe you can get skybrian or me to change the way we communicate to suit your preferences, but why? Nothing is gained. And if you spend your life trying to force people to communicate a certain way instead of trying to understand people, it's just going to be an endless war, where the only meaningful result you'll achieve is that it's harder for people to communicate with you. Is that what you want for yourself?
If you'd approached this from the perspective of "hey I think people would understand you better if you said it this other way", that would be fine. I think it's great to improve communication, and it's both up to speaker/writer and listener/reader to meet in the middle: both sides of this interaction can improve. But that's a pretty big difference between coming at someone like "you're wrong".
...because I think you'd write mathematical history if you can prove it?
> You're approaching this conversation with your own preconceived notions about what the words mean...
Peak meta, yes!
> ...to force your preferred usage of the words on other people.
Listen, currying is a defined term[0], as is partial application[1]. Those are primarily mathematic definitions, and your choice of words "...is a subset of..." is also stemming from a rather mathematical terminology if you'd ask me. Therefor its interpretation will primarily be itching logical intelligence.
Currying can be implemented by making use of a partial application, and partial application can be implemented by making use of currying a function. Neither one is a subset of the other and both are higher-order functions.
It was your choice of words with which you formed your statements, and no, I'm not going to discuss visual, linguistic, interpersonal, intrapersonal or naturalistic understandings with you on Hacker News about your intentions, I'm truly sorry. Your point "about the concept of currying" doesn't exist (yet AFAIK), feel free to imagine whatever higher concept you desire.
Even the confusion between currying and partial application is well known[2][3] and also documented on Wikipedia - you put in a prime example out of the confusion, thank you. You still double down on false statements, fine, use your languages however you want and feel free to interpret "I think you confuse currying with partial application." as a behavioral problem. It's truly amazing.
> ...because I think you'd write mathematical history if you can prove it?
No, because this is an informal conversation not a mathematical paper.
Clearly you understand what is meant if someone says that subtraction is a subset of addition. You also clearly understand they're not saying it as a statement of proof.
If you can understand someone well enough to correct their usage of words, you understand them well enough that they communicated successfully. So why create a problem?
> Listen, currying is a defined term[0], as is partial application[1]. Those are primarily mathematic definitions, and your choice of words "...is a subset of..." is also stemming from a rather mathematical terminology if you'd ask me. Therefor its interpretation will primarily be itching logical intelligence.
Yes, I'm well aware, I just don't care. People use words outside their dictionary definitions all the time and if we understand it, it doesn't matter.
Perhaps you should look up the word pedant? Dictionary authors are well aware of the pitfalls of relying on definitions as prescription rather than description.
> I'm not going to discuss visual, linguistic, interpersonal, intrapersonal or naturalistic understandings with you on Hacker News about your intentions,
Well, that's your prerogative, but I'll note that you haven't said anything interesting about currying because you keep assuming your audience knows less than they do. Learning to listen at an adult level would help you to have more interesting technical discussions, too.
Okay, that's a fair critique, let it not be said I can't admit when I'm wrong. I shouldn't have used `curry` as a verb there, as my point was about the concept of currying rather than the specific curry function.
My mistake there doesn't negate my larger point, but I've gotta go touch grass.
Currying refers to building (effectively) functions of multiple arguments out of only one argument functions (such as in a language that has only those).
A curried function is not understood to have been applied, partially or otherwise; none of its arguments have been bound to values, thereby reducing its arity.
Partial application is an operation that requires one or more arguments. Currying does not require arguments; rather, the result of currying requires arguments (all of them).
There is a sort of partial application (effectively) going on when the curried function is applied to the arguments, which has to be done one argument at a time through the chain. At each step, one more argument is bound, and fewer remain.
Currying and partial application are dualities (think of them as function introduction/function elimination), so neither subsumes the other one.
However, I agree with the wording of the original comment: when you _pass_ some arguments to a constructor and then to a method, this is (at least conceptually) partial application.
You can think of them as equivalent ways to write the same code. There are mechanical rewrites to go from one to the other, and these rewrites work in any direction. Which way is more readable depends on circumstances.
When a function needs a lot of parameters to do a calculation, these parameters can be arbitrarily grouped into records and supplied in any order. Languages have convenient syntaxes for certain ways of grouping parameters together.
For example, a closure creates a group of parameters in the form of a function.
* more difficult to distinguish arguments and return values (there's a reason most languages have distinct syntax for them)
* it encourages you to put your arguments in an order that might not be the most logical
And on top of that it only works for the last argument(s).
Feels like a lot of disadvantages to allow an overly clever trick that you can do in other languages much more readable using lambda functions. And those work for any arguments, not just the last one(s).
I much prefer partial application. It just makes more intuitive sense - you take a function with many arguments, "fix" some of those arguments, and get a new function out. No need to mess with argument ordering, or all that.
It's a shame the only language that did partial application well is, weirdly, Python.
Partial application just means fixing some arguments. My point was you can do that easily using lambda functions.
I guess you were talking about `functools.partial`, which is basically the same as currying and also only works on the last arguments. It's better to use lambda functions.
Also Python is far from the only language to have a function like that, e.g. see C++'s `std::bind` which nobody* uses since lambdas were supported.
In my entire life as a programmer, this is the one thing that I've found most confusing. Our old codebase used a lot of curried Ramda functions and nobody on the team could read any of it. We spent a week ripping every single instance of it out and replaced it with regular lodash and never looked back and never had an issue with it again.
After like ten or fifteen hours of trying to understand what currying is, I still have no idea, and frankly don't care. If I see it in a codebase it'd just be a big red flag to avoid that job altogether.
In Haskell it is easy. If you "forget" the last argument to a function, you get returned a function where you can provide that later on. A bit like saying "you can fill this in later". That is a "curried" function.
Example
add 1 2 // add is curried, you can use it like this, the "normal" way, returns 3
p = add 1 // since add is curried I can also provide just the first argument
p 2 // and then apply the last argument to the intermediate results, returns 3
What is the point of using curried functions in JS? I am not really sure. It is not very ergonomic and I wouldn't like to use them in general. Maybe for some specific things it could be useful.
In Haskell curried form is the default and the syntax and semantics really suits it. In JS non-curried is the default and it just looks odd, and you need libraries to support it. That library you mentioned doesn't look nice to use.
There is a sliding scale and even at the Haskellers have a limit of how much point-free they can take.
In JavaScript use of the style is problematic in another way: unlike Haskell in JS the length of the argument list is variable, which means that if someone adds another argument to either the caller or callee it can break the code in subtle and unexpected ways.
For this reason it’s good practice to always wrap functions being passed as arguments to another function in a lambda expression.
i.e instead of writing: g(f), you should usually write: g(x => f(x)) unless you have good reason to believe that g(f) is safe. This makes it difficult to use point-free style at all in JS.
For example arr.map(f) is generally unsafe in JS because if `f` adds an extra default argument of type number then your code will break and even TypeScript won’t let you know.
Honestly, a lot of these 'advanced' features aren't a good choice in most codebases IMO. The vast majority of 'enterprise' coding is CRUD and glue, where execution speed doesn't matter and being as simple and explicit as possible is the highest virtue. And the significant number of professional coders who worked hard to get their heads around things like pointers and printf format specifiers are still plenty productive in those kinds of codebases.
Things like currying are fun but like anything that encourages gratuitously deep call trees, they wreck your locality of reference (as a developer) and force you to 'decompile' the code in your head in order to understand it. I'm sure that a top level developer would be able to write curried JS in such a way that it was clear and readable to another top level developer, but that's not the point. The code's not for you, it's for newbie who gets stuck with it when you move on.
What if you forget, not in quotes, to give the second argument? Why on earth would you want to get a type error in a completely different part of the code because you got a function instead of an integer? Wouldn't it be desirable to have the compiler just tell you that you forgot the second argument where you forgot the second argument?
Is it really valuable to be able to do:
p = add 1
...instead of:
inc = (\x -> add 1 x)
...or, heaven forbid:
inc x = add 1 x
...?
I mean, I get the idea, they're following the lambda calculus, but this is one of the things that should have been dropped when they started to expand the lambda calculus to a general purpose programming language.
> Because possibly you return the partial assuming it's a number, and try to use it somewhere else, possibly even in another file.
Sorry, I must've been more explicit instead of implying certain usage patterns. What I meant here is that I have a hard time imagining this happening because I would start working on a function by writing its type signature. Unless my types check out, I won't be able to mark this function as "done" and jump to another part of code. So the situation "you return the partial assuming it's a number" simply can not happen, that's exactly what type checking is for. By the time I use it in another place, it has to already have been type checked.
Sure, but that usage pattern is not without cost. You're basically eschewing relying on type inference across function boundaries.
I think writing out type signatures is The Right Way, but it does take time, and The Right Way doesn't always happen on complicated projects with close deadlines.
But I don't think this is a problem in practice for whatever reason. Haskell does have a lot of "big scary type error, wtf" type problems, but not from currying. Or at least it did have when I last used it 5 years ago, it may have improved in the compiler error message front.
Also it is better than JS anyway (low bar):
> function add(a,b) {return a + b}; add(1);
<- NaN
> After like ten or fifteen hours of trying to understand what currying is
I have doubts on whether this is a sincere attempt at understanding the concept. The linked wiki page is like a five minute read and that's enough to understand the concept at a user's level.
What I suspect is that something else is tripping you up and causing code readability issues; but since you didn't know what currying is you incorrectly ascribed currying as the problem.
Heh, so much of the world runs on basic business software. If it ran fine after the rewrite, with much less cognitive load, why not? These were just bog standard web pages that someone overengineered the hell out of, to a point that nobody else could understand or maintain it. Sure, that might win them l33t points, but wasn't very useful otherwise.
As a Haskell programmer, I have to agree with you.
When you're writing Haskell, you should write idiomatic Haskell. Currying is part of that. It's a trivial concept and we use it liberally at work. If you're writing JavaScript though, I think it's a different story. I don't think JavaScript lends itself to FP in the way that Haskell or Elm does, and trying to make it do that only makes a project harder to work on.
Almost as if an astronaut can come, do their thing in a whirlwind and leave for another company 2 years later, while the remaining plebs have to figure it all out.
Yeah, a lot of the codebase was like that. Some lone wolf mad genius that occasionally left pieces of pure brilliance behind, but more often than not, just wrote code that was incredibly idiosyncratic and unnecessarily reinvented. Like he'd spend several files making a color gradient visualization system with his own scales, with an enthusiastic but limited understanding of color theory, that ended up producing mostly reasonable but occasionally insane color scales. We spent several days of back and forth discussions with him before understanding what he was actually trying to do (which isn't what his code was actually doing) and then replaced it with a one liner out of a standard visualization/color lib.
The entire codebase was like that. It stood out to me as the work of someone who was very smart but worked alone and never had to work with a team, and never considered what someone trying to retrace his mental process would have to go through. He left zero documentation or comments, built the whole thing in a rush, sold it to another company, and we were the ones hired later to clean up his mess.
Some of it was very good. He was really good at efficiently encoding low level network traffic over packet encodings he invented or heavily modified. Seemed like he would've been a brilliant signals engineer or cryptologist. But having to work with his ordinary business logic code was pretty nightmarish, if interesting from a reverse engineering and psychology point of view.
It's just not how I would ever want to write code meant for mere mortals. But then again, he's a multi millionaire now and I'm a rando nobody living paycheck to paycheck, so I'm in no place to judge lol.
> But then again, he's a multi millionaire now and I'm a rando nobody living paycheck to paycheck, so I'm in no place to judge lol.
If he had not made such a big mess, you might not be getting this paycheck to clean it up. I think they call this “creating scope for other people” at big companies, real staff engineer stuff. =)
I'm fortunate my parents didn't stick with free-to-play, but were willing to spend 5-6 digits more to buy a house in the "good" school district: https://news.ycombinator.com/item?id=39946026
I grew up during the Cold War, so I don't know what the Young Pioneers were learning*, but we were good little capitalists, and we learned the Golden Rule:
"Whoever has the gold makes the rules"
(*maybe the ones who spent their summers at Artek were learning "whoever makes the rules has the gold"?)
On that note, I found Orwell's posthumously-published Such, Such Were the Joys (about his boarding school days) suggests the inspirations behind large swaths of 1984.
In my experience, functional programming really shines in rapid prototyping. However, in production code I would try to avoid it, because it can confuse developers that are not used to it and also sometimes I struggle myself when decoding the short "clever" one-liner that I wrote 5 years ago. But if you want to write very powerful code withing a single line it is really great and makes you very productive for finding solutions fast. It is a bit like the Perl throw-away code we were writing in the 1990s. ;-)
The Ramda docs, primarily, and Google searches. It just seemed like a lot of unnecessary complexity. We modified the parts we understood (or believed we did), and just rewrote the rest from scratch. The tests still passed, nobody else complained, the managers and customers never noticed, the devs were much happier, QA never said a word, and a whole class of bugs disappeared once we were able to reliably predict the output of simple helper functions. Shrug. Seemed like a win win. Probably the best thing we ever did to that codebase.
Was the codebase in strict Typescript? I find currying to be sometimes useful in the right language / paradigm, as long as you can rely on the compiler to yell at you until you get it right.
Dynamically typed functional retrofit on with no type checking though? Hell no, that's a write-once read-never mess of unmaintainable shite.
(I also have been on a team with The Guy Who Wanted To Ramda, and I'm someone who writes build scripts in ocaml)
It was a mishmash. The parts written by later employees were strongly typed and easy to follow. The parts he wrote weren't typed at all and frequently relied on implicit conversions and bitwise operators to do magic operations that would take us mere mortals several days to even begin to understand, all just to save what would've been like 3 lines of code and a single comment.
Inheriting that codebase felt like watching one of those serial killer dramas... obviously a really smart guy, but with a psychology unlike most people I've ever met, and really hard to follow as a normal person. I still see him with a mixture of respect, awe, curiosity, and wonder.
The thing is, none of the stuff he gave is was actually complicated. It's just a basic business dashboard. But he reinvented almost every single part of it in totally nonstandard ways that the whole project was less about coding and more about anthropology and forensic psychology.
Currying is only a part of the functional programming stack. It can make your life easier when you combine it with other functional programming techniques like tacit programming. e.g. you have a function with a very long arguments list and dont want to write the arguments over and over again, but still dont want to define an additional function.
when you use tacit programming and currying together you can write code in a "bash-like" pipe style which not only makes the program more readable but also is less error prone.
I think a lot of people would disagree that they make code more readable. These are all ways of removing the names of intermediate results. Some names are noise, but more often, they’re useful documentation.
You can make code much shorter by removing all the application-specific names and only using very abstract, domain-independent names or symbols. The result can be seen in complicated regular expressions, array languages, bash scripts, and so on. In the wrong hands, these are all notoriously unreadable.
But you can also go to the other extreme and give every little thing a very long name, and that’s less readable in its own way. Bob Nystrom wrote a nice article about how to combat that tendency. [1]
Naming things is an art. In functional languages, let expressions are convenient and useful. When you feel like you don’t want to define another function, sometimes it’s better to resist that tendency and think about a better name.
You can have partial function application without currying. The problem with currying is that it's implicit, and positional. Two very bad things in programming.
As I said in another comment down below. Currying is great for rapid prototyping because you dont have to define a new function. eg. add1 and add(1) which you then can use for map/reduce or pointfree programming. In production code it often makes more sense to be a bit more detailed and verbose to make it readable for devs who are not familiar with currying. But abstraction in general is not something that is "bad".
high level abstraction can make you very productive, but it can also be rather unreadable. e.g. two code snippets in python (yes this is valid python code with some operator overloading):
# compute pi by drawing random numbers in a circle
I understand currying to be using a closure to "bake in" a value for a parameter.
Reading through these comments, it's clear that currying isn't clearly understood, which makes me doubt it's worth the cognitive load to use in a codebase.
Currying is one of those things in Haskell that always makes me think that Haskellers were so preoccupied with whether or not they could, that they didn't stop to think if they should.
In my limited Haskell experience, you can mostly ignore that functions are curried, but every once in a while, someone uses it, and it has never, in my experience, made the code easier to understand, because the currying happens implicitly.
The more general case of currying is partial application, where instead of a function taking only its first argument and returning a function that takes its next argument, you can apply a function any of it's arguments and return a function that takes the remaining arguments. So if you have:
div x y = x / y
Then:
invert = (\x -> div 1 x)
-- invert is a partial application of div to the 1 as first argument
halve = (\x -> div x 2)
-- halve is a partial application of div to 2 as the second argument
Obviously, the first one could be done with currying. What makes partial application better in my opinion is that it's explicit about what's happening, and doesn't happen without you asking it to.
I don't think currying happens without you asking to, though. It happens because it happens, it's part of the language, and it's something you implicitly keep in the back of your mind every time you see a function call. I don't program a lot in Haskell, only some maths things I sometimes might need since it is rather useful for that, but the concept of currying is so natural that it's constantly expressing itself in the code. Very rarely do you apply arguments and consider that to be a function call in itself instead of like, three function calls. And since partial application is so incredibly important to Haskell and other similar languages, without currying writing would be very difficult. Consider the actual simple example of
> I don't think currying happens without you asking to, though. It happens because it happens, it's part of the language, and it's something you implicitly keep in the back of your mind every time you see a function call.
Eh, but that's my point: I want less cognitive load. Currying is another thing that I have to keep in the back of my mind, and Haskell already has way too many of those, and it's not a particularly useful thing. I've got limited space in the back of my mind for things and if I'm going to keep things in the back of my mind I want them to be useful. I mean, if your argument in favor of currying is that it saved you a few keystrokes in that example, color me unimpressed.
Maybe I'm just too stupid to understand easily, but the "simple" example you're giving is taking me a while to understand. If a junior dev on my team submitted that in a PR I'd send it back asking them to break it up into a few smaller named functions and probably not use a partial application at all. Something like "I know it's fun to be clever but let's make this easier for the next person who has to figure out what it does".
I guess what I'm saying is that for that example it seems like you're going for tersity rather than clarity for future readers of the code. If you were going for clarity you probably wouldn't write it either of the ways you've given.
And in big projects clarity is the number 1 concern[1]. In toy examples like this I can slog through and figure something like this out, but when it's 30 functions written like this, all calling each other in the most clever ways possible, nobody can figure it out.
[1] EDIT: Okay #2 after correctness perhaps. But it becomes hard to achieve correctness without clarity as a project grows.
Hi, as someone who has been interested in functional programming for a while but who struggled to read point-free Haskell code until recently, I think it might be useful to share my perspective.
If this Haskell code does not look clear, it's because people are unfamiliar with point-free style, not because the this code is badly written, and especially not because the reader is stupid.
The given Haskell code reads naturally to me now. (I'm only a little uncertain because I can't write Haskell.) It would have been line noise to me before before point-free style clicked.
I tend to read Haskell code in articles from right to left, and it reads: "Choose the symbols whose first letter is '*', get their respective (neighboringNumbers numbers), and choose the results whose length is 2". I read (neighboringNumbers numbers) as if it were a noun.
I don't know how this clicked, but I'm pretty sure it has nothing to do with stupidity. Perhaps it was by chance, or perhaps it was by banging my head against the wall enough times.
Would I introduce point-free style in a JS codebase? Probably not. People are unfamiliar with this kind of style. Would I introduce this style in a Haskell codebase? Almost certainly, because it's clear and I think it reflects how Haskell programmers think.
They say that array languages are pretty readable once you get used to them, too? But you’re drastically limiting your audience. Part of readability is writing for people who aren’t as fluent as you are.
Expert jargon can sometimes be useful, but it often obscures things that would be pretty simple if written some other way.
With Haskell there’s a tension between saying “I only care about writing for other expert programmers” and “more people should learn Haskell.” The idioms are part of the turnoff.
It's not really saying "only expert programmers" though, is it? It's people who know Haskell, which by coincidence happens to be overzealous undergraduates and a certain subset of experienced programmers. FP is a paradigm among many, its basics somewhat predate (or since it's so close, co-date?) more imperative descriptions of computation. That we mostly use and as such mostly teach beginners with procedural languages is a quirk of history. Nothing would prevent a change to that except historical inertia. Saying "more people should learn Haskell" is saying "I don't want to write code only for other expert programmers." It's just as natural if you know it, and even beginners could know it, they just don't.
However as I mentioned, since it is not true that most people can read FP code, I mostly avoid using it. The example comes from my solution to AoC2023's Day 3, "Gear Ratios", which is just about the only thing I use Haskell for.
That doesn't mean that using it doesn't have practical applications, since being used to multiple paradigms opens you up to unconventional solutions. I've recently sped up a MATLAB function ~100x through using a more functional style to manipulate memory more efficiently. Async/await, certain styles of modern iterator manipulation and generators escaped F#, CLU and others into C# and from there into the world at large specifically because Microsoft programmers saw a problem they had had a solution for in previous functional projects. So it's not all useless.
For the record, a more imperative version could be written as
symbols = [ imagine there's stuff here ]
gears = []
for (symbol, coord) in symbols:
if symbol == '*':
ns = neighbouringNumbers(coord)
if len(ns) == 2:
gears.append(ns)
or in Python's functional-inspired notation which directly mirrors what's happening in the Haskell code
gears = [ neighbouringNumbers(coord)
for symbol,coord in symbols
if symbol == '*' and len(neighbouringNumbers(coord)) == 2 ]
though that requires an unnecessary extra call to neighbouringNumbers, which you could solve with a walrus op but I can't remember how to do that. I also changed the entire pair being passed to neighbouringNumbers (which was convenient in Haskell) to only the coordinate that is required (which is convenient in Python).
Personally I just find nowadays that having to comprehend "it collects neighbour-pairs from '*' symbols" from the imperative code harder than having that be the thing that is actually written down.
It’s true that readability is culture-specific, that if the culture were different and people learned different things then different languages would be more readable. But I still think there are differences between languages in the sense of how much you can understand without knowing the definition of every term.
For example, if you’re looking at Lisp code and you don’t know whether an outer term is a function, macro, or special form, you really don’t understand anything inside it except at the most superficial level. It might as well be JSON. Macros aren’t marked, so any unknown term might be a macro. (Knowing the surface syntax is still helpful, though. Well-known syntaxes for data are useful.)
With Forth it’s pretty bad, too. Not knowing what a single word does means that you don’t know what’s on the stack afterwards.
A Unix pipeline is a bit more orderly since you know that there are streams of bytes. You know the command names and the arguments to each command. But if you don’t know what a command does, you don’t know much at all about what the data looks like after that point.
I find currying and point-free programming to be pretty opaque because I can’t tell where the function calls are or how many arguments each function takes from usage. I don’t know what the dataflow looks like. It seems like you need to know some precedence rules too?
Languages with conventional function call syntax, augmented with named parameters, seem better. I can tell where the function calls are and where the inline functions are. Augmented with reasonable names for temporaries, I can make reasonable guesses about what the code does.
These syntax concerns seem independent of whether it’s a functional or imperative language? Making reasonable guesses is what we do when we read pseudocode. Maybe what I’m saying is that some syntaxes seem better for pseudocode than others, and I like languages that look like pseudocode better.
I suspect that these syntax differences also have an effect on how good the error messages are when you screw up.
> you really don’t understand anything inside it except at the most superficial level
This is true, but:
- the name of the operator is typically a word that you can easily search for in the documentation or on the web, or with "jump to definition" in your editor, if it's something locally defined in the source tree.
- you usually understand the shape of what is inside it. You know what parts of the program you are looking at are arguments to that mysterious operator and which are not. If asked which expression is the third argument of that operator, you can easily find it and know where it begins and ends.
> Macros aren’t marked, so any unknown term might be a macro.
Yes, that's a problem.
There are two clues:
* Macros typically have naming conventions. For example anything beginning with DEF should be a defining macro, not a function. Anything with DO- will be a control structure. Anything with WITH- will be a scoping macro. And so on. Avoid active names like CREATE- and use DEFINE- instead. CREATE-CLASS would be a function, DEFINE-CLASS would be a macro.
* In source code the enclosed code typically has syntax and special indentation, when used with macros. Functions have certain uniform indentation rules.
Above is a function and uses one of the typical formatting/indentation rules for functions. Line up the arguments. Start with the required argument and then the named argument pairs (name, arg).
The macro looks different. The first important things like name and superclasses are on the first line. The other parts of the class specification follow and are indented by two characters.
I guess my bigger complaint in this example is that there's a lot left out. What's in `symbols`? What is each `coord`? What does `neighbouringNumbers` do? What is this function trying to do?
I write Python a great deal for a living these days and the Python code isn't much clearer to me. In both the Python and Haskell examples I can tell what it's doing (except the opaque neighbouringNumbers)--I just can't tell why it's doing it.
Just a remark, currying is nothing that is special to Haskell, it's a general ML "speciality" including the syntax for function application of not using parens.
Which is because lambda calculus does not have functions with more than one parameter, I guess.
Yeah, I'd attribute this to the lambda calculus, for sure.
And in the lambda calculus, it makes a ton of sense, because it allows you to break proofs into smaller chunks more easily.
But in a general-purpose programming language where the proofs are automated, I think this just makes the code less communicative of programmer intent.
> Currying is one of those things in Haskell that always makes me think that Haskellers were so preoccupied with whether or not they could, that they didn't stop to think if they should.
This had to be said! Sure you can make the argument that this improves your productivity as a coder, or makes the code more concise, but to someone coming along cold and trying to pick apart your code to fix something it's going to be a nightmare.
I recently saw the title, Learn Physics with Functional Programming, and thought to myself... I know physics and functional programming but I want to learn Haskell.
Having gone through it, I highly recommend the book; especially to anyone knowledgeable about any 2 and interested in the third.
I love Haskell. I love writing it, and reading it. Haskell is a beautiful programming language. One where I felt immediately at home in and comforted by.
Currying is one of the foundational concepts of the language, but I do think it is one aspect of the language the community treats excessively sacredly.
In Haskell, function composition has right associativity. What this means, in the event of a function that takes in 3 Integers as arguments and returns a Double would have a type definition of:
FN_NAME :: Int -> Int -> Int -> Double
The right associativity then also means this is equivalent to these type definition:
fn0 :: Int -> Int -> Int -> Double
fn0 x y z = (fromIntegral (x + y)) / (fromIntegral z)
fn1 :: Int -> (Int -> Int -> Double)
fn1 x y z = (fromIntegral (x + y)) / (fromIntegral z)
fn2 :: Int -> (Int -> (Int -> Double))
fn2 x y z = (fromIntegral (x + y)) / (fromIntegral z)
ghci> fn0 1 2 3 -- 1.0
ghci> fn1 1 2 3 -- 1.0
ghci> fn2 1 2 3 --1.0
This is currying in action. A function that takes three arguments is the same as a function that takes one argument and passes it to function that takes two arguments.
As I understand it, the compiler sees ALL functions as a composition of functions of one input.
This is pretty cool, and a powerful foundation to build on. Where I take a bit of issue is when the Haskell community allows the rigor of the type definition to carry some of the weight of the expressiveness of the function definition.
Each function definition ('=') pairs with a type definition ('::'), as the ones above, ie. an example from the promoted book:
type R = Double
type VecDerivative = (R -> Vec) -> R -> Vec
vecDerivative :: R -> VecDerivative
vecDerivative dt v t = (((v (t + dt/2)) ^-^ (v (t - dt/2))) ^/ dt)
velFromPos :: R -- dt
-> (R -> Vec) -- position function
-> (R -> Vec) -- velocity function
velFromPos = vecDerivative
The second function definition: "velFromPos = vecDerivative" ; is equal to saying, "velFromPos dt v t = vecDerivative dt v t". Haskell seems to allows this syntactical ~implicit argument passing because of it's reverence for currying.
-- random example of 'vector-valued function'
v1 :: R -> Vec
v1 t = ((2 *^ (t**2 *^ iHat)) ^+^ (3 *^ (t**3 *^ jHat)) ^+^ (t**4 *^ kHat))
ghci> vecDerivative 0.01 v1 1 -- vec 3.999999999999937 9.000074999999885 4.000099999999962
ghci> velFromPos 0.01 v1 1 -- vec 3.999999999999937 9.000074999999885 4.000099999999962
This has some interesting effects, such as displayed above where the use of currying makes it explicitly clear that calculating the velocity is the exact same thing as calculating the derivative of position.
But I always felt this ~implicit syntax is needlessly aggressive to newcomers.
I also pathologically write my Haskell code like a Lisp because the Haskell community's celebration of the complex precedence structure of its order of operations causes practitioners to appear, to my eyes, over confident in their ability to sight read their own work's order of operations. ;P
I will forever think the second is easier to read, but if you look at many Haskell code bases you will see stuff like the first all over the place, and I have been unable to find a reason why the implicit function arguments and lack of parenthesis syntax is considered idiomatic.
In the face of the ability to write the functions explicitly and with expressive parentheses, it seems the answer is "because you can".
Wow what a lot of parentheses! To me the first example is perfectly clear because it follows the same precedence rules I learned for arithmetic at school, along with most programming languages. There's nothing special about Haskell here, in Python I would write:
You should see when negative numbers are involved.
Due to Haskell's rigor in regard negative numbers:
ghci> -1 + 2
1
ghci> 2 + -1
<interactive>:12:1: error:
Precedence parsing error
cannot mix ‘+’ [infixl 6] and prefix `-' [infixl 6] in the same infix expression
ghci> 2 + (-1)
1
ghci> -1 * 3
-3
ghci> 3^-1
<interactive>:3:2: error:
• Variable not in scope: (^-) :: t0 -> t1 -> t
• Perhaps you meant one of these:
‘^’ (imported from Prelude), ‘-’ (imported from Prelude),
‘^^’ (imported from Prelude)
ghci> 3^^-1
<interactive>:4:2: error:
• Variable not in scope: (^^-) :: t0 -> t1 -> t
• Perhaps you meant ‘^^’ (imported from Prelude)
ghci> 3**-1
<interactive>:5:2: error:
• Variable not in scope: (**-) :: t0 -> t1 -> t
• Perhaps you meant ‘**’ (imported from Prelude)
ghci> 3**(-1)
0.3333333333333333
ghci> sqrt 2 + 1 * 3 + 3 * 2 + 1 / 7
10.557070705230238
ghci> sqrt $ 2 + 1 * 3 + 3 * 2 + 1 / 7
3.3380918415851206
ghci> -sqrt 2 + 1 * 3 + 3 * 2 + 1 / 7
7.728643580484048
ghci> -sqrt $ 2 + 1 * 3 + 3 * 2 + 1 / 7
<interactive>:25:1: error:
• Non type-variable argument in the constraint: Num (a -> a)
(Use FlexibleContexts to permit this)
• When checking the inferred type
it :: forall {a}. (Floating a, Num (a -> a)) => a
ghci> sin -1
<interactive>:7:1: error:
• Non type-variable argument in the constraint: Num (a -> a)
(Use FlexibleContexts to permit this)
• When checking the inferred type
it :: forall {a}. (Floating a, Num (a -> a)) => a -> a
ghci> (sin -1)
<interactive>:10:1: error:
• Non type-variable argument in the constraint: Num (a -> a)
(Use FlexibleContexts to permit this)
• When checking the inferred type
it :: forall {a}. (Floating a, Num (a -> a)) => a -> a
ghci> sin (-1) -- -0.8414709848078965