Home > other >  Haskell Programming Style Preference
Haskell Programming Style Preference

Time:09-30

I've been doing Haskell for awhile (small projects ~ 3K LOC) and I still feel like a newbie. I don't believe that I have a good Haskell style; I generally go for map/filter/fold. No fancy monads / applicatives etc.

I'd like to improve. I had a simple requirement to generate the sub-harmonics of 377 megahertz, and print it out in table form with 8 columns (arbitrary) so I wrote it three ways. (I know I could use the 'boxes' package but this was an exercise for me).

I'd really like feedback as to which would be 'preferred' or another different way of doing it that's more 'Haskell'. (I found the list comprehension the most difficult as I was trying to do it without a 'map')

I was proud of myself .. for the first time I used an applicative!

Comments appreciated, including places I could see good Haskell style. I've looked at large packages (i.e. Megaparsec) and they use tricks and language extensions, that are difficult for me to follow. I'd like to be able to understand them eventually but it's overwhelming right now.

Thanks!

Tom

import Data.List (intercalate)
import Text.Printf
import Data.List.Split (chunksOf)

gen :: [Float]
gen =  pure (/) <*> [377] <*> [2,3..50]

main :: IO()
main = do

    -- Try One --  ... List function
    let ps =  map (\f -> printf "%7.2f\t" f) gen
    putStr $ concat (intercalate ["\n"] (chunksOf 8 ps))

    putStr "\n"
    
    -- Try Two -- ... IO Map
    mapM_ (\xs -> (mapM_ (\x -> printf "%7.2f\t" x) xs)
                       >> (printf  "\n")) (chunksOf 8 gen)

    -- Try Three -- ... List Comprehension
    putStr $ concat [ ys' | ys  <- (chunksOf 8 gen),
                            ys' <- (map (\y -> 
                               printf "%7.2f\t" y) ys)    ["\n"] ]


CodePudding user response:

  • I'm a fan of Applicative style, but even I would write gen as map (377 /) [2..50].

  • All three versions write (\x -> printf "%7.2f\t" x), which is just a more complicated way to write (printf "%7.2f\t"). The fewer lambda parameters there are to mentally keep track of, the better.

  • Most of the effort in versions 1 and 3 is struggling to interleave the newlines in the middle of the string. But there's already a library function for that: unlines. With this function, the whole problem is easy:

    main = putStrLn . unlines . map renderRow . chunksOf 8 $ gen
      where renderRow = concatMap (printf "%7.2f\t")
    

    Save your brainpower for other problems.

  • I don't like version 2: it interleaves IO and pure code, while 1 and 3 stay nicely in the pure world. If you are going to do it this way, though, I would use for_ and do instead of mapM_:

    for_ (chunksOf 8 gen) $ \line -> do
      mapM_ (printf "%7.2f\t") line
      putStr "\n"
    

    I think the indentation and line separators help a lot with making the structure apparent, compared to the parenthesis and lambda soup of mapM_. My personal rule of thumb is that I don't like to pass lambdas to functions in the map family. It's great for existing functions, or for partial application, but if you have to write a lambda you're probably better off with a list comprehension or something.

  • Version 3 seems okay, but it's weird to see a map in the middle of a list comprehension binding. One thing to consider: if you struggle to write list comprehensions, maybe give do a try. I know I personally have trouble writing list comprehensions because I have to write the "body" first, before I've figured out what variables I'm binding. So instead of [f y | x <- whatever, y <- something x], I usually write

    do
      x <- whatever
      y <- something x
      pure $ f y
    

    This makes it more of a hassle to have filter conditions - you have to import Control.Monad.guard - but I like the less dense layout and the reversed ordering.

  • Related