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
asmap (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_
anddo
instead ofmapM_
: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 themap
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 givedo
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 writedo 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.