Question

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 >>>

Was it helpful?

Solution

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
Licensed under: CC-BY-SA with attribution
scroll top