6 ms·
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 i
by kamray23 2y ago
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
gears = filter ((==2) . length)
. map (neighbouringNumbers numbers)
$ filter ((=='*') . fst) symbols
which without currying would have to look like
gears = (\xs -> filter ((\x -> x == 2) . length) xs)
. (\xs -> map (\x -> neighbouringNumbers numbers x) xs)
$ filter (\(c,_) -> c == '*') symbols
It just makes partial application a lot easier, especially when this kind of code pops up all over the place.
- kerkeslager 2y ago> 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.
- nightlyherb 2y agoHi, 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. gears = filter ((==2) . length) . map (neighbouringNumbers numbers) $ filter ((=='*') . fst) symbols 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.
- skybrian 2y agoThey 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.
- kamray23 2y agoIt'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.