strSum = sum . map read . words
strSum = sum . map read . words
I much prefer this over 10 levels of class indirections or procedural style.
Don't get me wrong, I like FP and have been trying to get into it for a long time. But currently I strongly believe FP as commonly done in Haskell is just too far from what we expect even before we start writing code. Combining functions and chaining Monads just seems to me to be extremely hard to do and understand, and I don't need to do any of that in "lesser" languages. However, I am finally "getting it" with newer languages like Flix and Unison - they let me just use `let` and stuff like that which makes the code trivial again, while being basically purely functional.
Who's "we"?
I spent years writing JavaScript, PHP, and Ruby. I thought Haskell was weird and hard, and probably not practical in the real world.
As it turned out, I was just being a fool. Once you actually learn it, you realise how silly the opinions are that you had of it before you learned it.
Advent of Code is running right now. Why don't you just try learning the language?
everyone is familiar with "I don't know how exactly, but generally it would be this way..., we can discuss specifics later" which is the same as reading the above pointfree notation (sum . map read . words) verbatim instead of imperatively inside-out: something is a sum of all parsed values of space-separated words.
I’m guessing you do use languages that are very similar to JS. Like a Spanish speaker saying “I don’t speak Italian or Chinese but Italian is way easier.” If you wrote F# every day you would probably find Haskell syntax quite intuitive
I know a dozen languages well. Everyone here thinking it's just ignorance, but that's not the case. There's just no way that, for me, Haskell and similar languages are readable in any sense just because they're more concise. If that was the case Haskell still wouldn't be close to the most readable, but something like APL or Forth would. I've tried for more than 10 years to be like you guys and read a bunch of function compositions without any variable names to be seen, a few monadic operators and think "wow so easy to read"... but no, it's still completely unreadable to me. I guess I am much more a Go person than a Haskell person, and I am happy about that.
There's no such thing as a "Go person" or a "Haskell person"; all programming languages are made up. Nobody has "more natural inclination" for coding one way than another. Just get your ass out of the comfort zone, try learning a new (to you) language - give it a heartfelt attempt to use it - that may change your life.
Just to be clear - I'm not saying Haskell is easy, oh no, not at all. I'm just saying that it stops being so intimidating and weird after a while.
In what sense?
Haskell's: strSum = sum . map read . words
in JS would be: const strSum = str => str.split(' ').map(Number).reduce((a, b) => a + b, 0);
for a person who's not already a JS programmer, the first one would be more readable (without a doubt), it literally reads like plain English: "sum of mapped read of words".
Haskell's version is more "mathematical" and straightforward. Each function has one clear purpose. The composition operator clearly shows data transformation flow. No hidden behavior or type coercion surprises.
Whereas JS version requires knowledge of:
- How Number works as a function vs constructor
- Implicit type coercion rules
- Method chaining syntax
- reduce()'s callback syntax and initial value
- How split() handles edge cases
So while the JS code might look familiar, it actually requires more background knowledge and consideration of implementation details to fully understand its behavior. Javascript is far more complex language than most programmers realize. btw, I myself don't write Haskell, but deal with Javascript almost daily and I just can't agree that JS is "more readable" than many other PLs. With Typescript it gets even more "eye-hurting".
It's funny to me that you quote the FP-like version of that in JS.
The more traditional version would be more like this:
function strSum(str) {
let words = str.split(' ');
let sum = 0;
for (word of words) {
sum += new Number(word);
}
return sum;
}
I do sincerely think this is more readable, no matter your background. It splits the steps more clearly. Doesn't require you to keep almost anything in your head as you read. It looks stupid, which is great! Anyone no matter how stupid can read this as long as they've had any programming experience, in any language. I would bet someone who only ever learned Haskell would understand this without ever seeing a procedural language before.- The assumption that "verbose = readable" and "explicit loops = clearer"? Seriously?
- The suggestion that "looking stupid" is somehow a virtue in code? "Simple" I can still buy, but "stupid"... really?
- You're using new Number() - which is actually wrong - it creates a Number object, not a primitive;
- You `sum +=` is doing not a single op but multiple things implicitly: addition, assignment, potential type coercion, mutation of state;
- for loops are another layer of complexity - iterator protocol implementation, mutable loop counter management, scoping issues, potential off-by-one errors, break/continue possibilities, possible loop var shadowing, etc. Even though for..of is Javascript's attempt at a more FP-style iteration pattern and is safer than the indexed loop.
You clearly underestimate how natural functional concepts can be - composition is a fundamental concept we use daily (like "wash then dry" vs "first get a towel, then turn on water, then...").
Your "simple" imperative version actually requires understanding more concepts and implicit behaviors than the functional version! The irony is that while you're trying to argue for simplicity, you're choosing an approach with more hidden complexity.
Again, I'm not huge fan of Haskell, yet, the Haskell version has:
- No hidden operations
- No mutation
- Clear, single-purpose functions
- Explicit data flow
You have just demonstrated several key benefits of functional programming and why anyone who writes code should try learning languages like Haskell, Clojure, Elixir, etc., even though practical benefits may not be obvious at first.
On the contrary French is much more readable than English to a Spanish speaker. Because French is much more similar to Spanish than English is.
Same with your JS example, I would guess it is much more similar to what you are used to
The more dense the code, the more there is to unpack until you are deep in the language.
It's OK for languages to be more verbose and offer structural cues (braces) as this often helps in human parsing of logic.
You don't do programming with chalk on a wallboard, for crying out loud. Ideally, you are using a good IDE with syntax completion. Therefore, readability matters more than the ability to bang out commands in as few keystrokes as possible.
It's about phase transitions. When you understand the system, shorter symbols are easier/faster to reason with. If your primitives are well thought out for the domain, this notation will be the optimal way of understanding it!
On the other hand, longer names help on board new people. Theoretically, you could avoid this issue by transforming back and forth. Uiua e.g. lets you enter symbols by typing out their names. Typing "sum = reduce add" becomes: "sum ← /+". If you transform it back...
Imagine if you could encode the std lib with aliases!
You've made a common mistake. You're wiring your listener's thinking with the imperative inside-out approach that you're used to. Instead, it should be explained as this: "strSum = sum . map read . words" is "a sum of all parsed values of the original input of space-separated words". The reason you should avoid inside-out explanations is because in Haskell you're allowed to move from general ideas to specifics, and you can sprinkle `undefined` and `_` for specific details whilst thinking about general ideas and interfaces.
words :: String -> [String]
So that words "foo bar baz"
-- Produces: ["foo","bar","baz"]
In my experience, both the blessing and the curse of Haskell's incredible succinct expressiveness is that, like other specialised languages - for example using latin for succinctly-expressed legal terms - you need a strong understanding of the language and its standard library - similar to the library of commonly used "legal terms" in law circles - to participate meaningfully.Haskell, and languages like Go (which anybody with a bit of programming experience can easily follow) have very different goals.
Like many in this discussion, I too have developed a love/hate thing with Haskell. But boy, are the good parts good ...
So maybe there a difference where Haskell has an advantage? I mentioned it in my previous comment but I don’t know Haskell at all, but if this is “the way” to do splits by word then you’ll know both to read and write it. Which would be a strength on its own, since I imagine it would be kind of hard to do wrong since you’ll need that Haskell understanding in the first place.
Someone thought "words" was the perfect name, and it wasn't me!
As for "words"... yes, possibly not the best name. But also so common that everyone that has ever written any Haskell code knows it. Such as Java's System.out.println
find . -name '*.py' | sed 's/.*/"&"/' | xargs wc -l
But instead of using | to tie the different functions together you're using . and the order is reversed.Yes, that definitely the case.
If you know what each function above does, including the function composition dot (.), then this is like reading English — assuming you know how to read English.
There are other languages which are functional as well. like the one in the article and like Elixir where readability is it sacrificed.
I still think readability is atrocious in this language. Sure I can get used to it, but I’d never want to subject myself to that
strSum = sum . parsed . words
where
parsed = map read wordsPerLine = filter (>0) . map (length . words) . lines
Funnily enough, parenthesis are actually optional in Elixir, although it's a warning to use pipe syntax without them. The following is valid in both Haskell and Elixer: length [1,2,3] apply foo bar (lol wat)
in Haskell, and it confuses you, simply mentally replace it with apply(foo, bar, lol(wat))
To translate into a more popular syntax (e.g. JavaScript). strSum = words >>> map read >>> sum
For most people this then becomes "Apply `words` to the input argument, pass the result to `map read` and then `sum` the results of that".I don't think `.` is super complex to read and parse, but we had people new to Haskell so I thought it prudent to start them off just with `>>>` and keep it that way. Most things are read left-to-right and top-to-bottom in a codebase otherwise so I don't see why not.
Edit:
I also told everyone it's fine to just spell out your arguments:
stringSum sentence =
sentence
& words
& map read
& sum
In the example above `&` is just your average pipe-operator. Not currying when you don't have to is also fine, and will actually improve performance in certain scenarios.Edit 2:
The truth is that there are way more important things to talk about in a production code base than currying, and people not using currying very much wouldn't be an issue; but they'll have to understand different pointer/reference types, the `ReaderT` monad (transformer), etc., and how a `ReaderT env IO` stack works and why it's going to be better than whatever nonsense theoretical stack with many layers and transformers that can be thought up. Once you've taught them `ReaderT env IO` and pointer types (maybe including `TVar`s) you're up and running and can write pretty sophisticated multi-threaded, safe production code.
2. Reverse
Now you have:
words | map read | sum
Or..
$ cat words | map -e read | sum
I do like pipelines though!
words |> map read |> sum
IHP uses it a lot.