main :: IO () main = Q.getContents -- raw bytes & AS.parsed lineParser -- stream of parsed `Maybe Int`s; blank lines are `Nothing` & void -- drop any unparsed nonsense at the end -- [1] & S.split Nothing -- split on blank lines & S.maps S.concat -- keep `Just x` values in the sub-streams (cp. catMaybes) & S.mapped S.sum -- sum each substream & S.print -- stream results to stdout
There's still quite a bit that can be improved here.
First of all: comments are good. But whenever you write a comment, ask yourself "could I choose a better name instead?" Make the code self-documenting at all usage sites by choosing a good name once. It's a good idea for every language, but this piece of code is a good example of how to apply it. So, first step:
main ∷ IO () main = Q.getContents & parseLines & dropUnparsed -- Add the explanation/link definition side & splitOnBlankLines & catMaybes' -- why write "it's catMaybes", when you could just write "catMaybes"? & S.mapped S.sum & S.printDo you need more functions this way to store the names? Yes. Which is a good thing, because they might be reusable. Of course there's a limit; if every function fits in half a line, you've probably gone too far.
Second step: Thinking from left to right is a remainder from
thinking imperatively. If you just turn around the top level of
the code, the reader is forced to a game of ping pong while
reading, so it can even make it harder to understand. So let's get
rid of that (&) crowbar.
main ∷ IO () main = S.print . S.mapped S.sum . catMaybes' -- by the way, there's probably a better name for what it's actually doing. "dropBlankLines", maybe? . splitOnBlankLines . dropUnparsed . parseLines $ Q.getContents
There's more reasons why going against Haskell's natural grain is
a bad idea, and you provided the perfect hook to talk about it:
(|>): a -> (a -> b) -> b (|.>): (a -> b) -> (b -> c) -> c (|$>) f a -> (a -> b) -> f b (|*>) f a -> f (a -> b) -> f b
Operators have the inherent problem that there aren't many symbols
to choose from. That means that almost all operators are
overloaded. For example, (|>) will be confused with
the one from Data.Sequence. Yes, there's unicode. I love unicode
(see that sneaky little "∷" up there?) so I've tried using Unicode
in operators before, but one single person using the project had
their device set to a C locale and so my whole library was
search-and-replaced. It's still 1968 out there, so there's like 10
symbols to choose from.
And even if you could use more symbols, operators still have the
inherent problem that they can't contain any letters. What exactly
does (>>?$>) mean? Or (|>||>)?
Or (<<*>@)?
So why not rely on the operators that are widely used instead of
crowbaring new ones in just so that we can keep programming in C.