8

Below is a line that handles socket connections in a simple Haskell program.

mainLoop :: Socket -> IO ()
mainLoop sock = accept sock >>= forkIO . handle . fst >> mainLoop sock

The "flow" of data in the function is not consistent: it's left to right when using bind >>= or >> (sequence?), but right to left when using function composition ..

Is the below better? Using a simplified/modified form of >>> from Control.Arrow, which is a flipped function composition.

infixr 2 >>>
(>>>) = flip (.)

mainLoop :: Socket -> IO ()
mainLoop sock = accept sock >>= fst >>> handle >>> forkIO >> mainLoop sock

It has a consistent data flow, left to right. Are there any drawbacks to using this as a go-to pattern for most code?


An alternative, as discussed in comments, is to use =<<. But it looks like the flipped form of >> is also not defined.

infixr 1 <<
(<<) = flip (>>)

mainLoop :: Socket -> IO ()
mainLoop sock = mainLoop sock << forkIO . handle . fst =<< accept sock

So actually I have the same question for this form as for the form using >>>

5
  • 2
    Why not just use =<<? Commented Mar 11, 2017 at 0:51
  • @ReinHenrichs ah I didn't think of that / wasn't fully aware of it! Thanks! Still, there is still a bit of a reverse flow compared to my intuition of top to bottom, left to right. If nothing else, the type signature of functions are left to right, and =<< would be right to left. Commented Mar 11, 2017 at 8:40
  • @ReinHenrichs Ah also, << doesn't seem to be defined (of course I could define it) Commented Mar 11, 2017 at 9:23
  • =<< is a form of application. >>= is a form of reversed application. If you're used to applying a -> b functions in the direction of $, you can get used to applying a -> m b functions in the direction of =<<. Also, << is defined as <* in Control.Applicative. Commented Mar 13, 2017 at 21:18
  • Using . is far more readable than >>>.
    – mrr
    Commented Mar 14, 2017 at 21:16

1 Answer 1

4

I think the real answer is

  1. If you have any sort of colleagues on this project, run this by them to get their opinion.
  2. If it is just for you, pick whichever you like most.

While Haskell supports point-free programming very well, the language and community have more or less standardized around math-based right-to-left composition with (.) and ($). Unfortunately as you noted, these chains are frequently less readable than their flipped left-to-right counterpart.

Now it is pretty easy to defined flipped versions of the above operators but the difficulty is that there is no real convention. You can use (>>>) from Control.Arrow. I have also seen (#) in diagrams or (&) in Data.Functions for flip ($). And it is of course only a couple of lines to define your own versions. Because there is not a single standard answer here you'll run into a couple of problems:

  1. Unclear precedence. (.) and ($) have very clear and well known precedence: highest and lowest respectively. The precedence of any flipped version will not be so familiar (readers of your code will not know immediately how to parse the expression).
  2. Cognitive burden. There is a small but definite cost for using unfamiliar notation -- readers have to keep reminding themselves what they are reading.
  3. Unfamiliarity. While code using any flipped versions could be totally valid and working Haskell code, it will not look exactly like normal Haskell code.

I run into these problems in both of your cases. In the first case, the compresence of (>>>) and (>>) makes visually parsing the line difficult. Because (>>) is quieter I want to group the last two expressions together even though that is not correct. The second version has the problem that reading a right-to-left monadic chain is just weird looking. Again there's nothing incorrect about either version, but I think they make your code less idiomatic for little gain.

I think the real solution (other than getting a time machine and redesigning Haskell's standard library) is that whenever you run into a chain (or in this case a chain-of-chains) that is difficult to read; rather than futzing with directions or anything it is best just to rewrite / decompose that expression. Here I might suggest

mainLoop :: Socket -> IO ()
mainLoop sock = do
  contents <- accept sock
  forkIO . handle . fst $ contents
  mainLoop sock

Or even better

mainLoop sock = forever (accept sock >>= handleContents)      
  where
    handleContents (h, _) = forkIO . handle $ h

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.