Should do-notation be avoided in Haskell?

前端 未结 7 1909
时光说笑
时光说笑 2020-11-29 01:32

Most Haskell tutorials teach the use of do-notation for IO.

I also started with the do-notation, but that makes my code look more like an imperative language more th

7条回答
  •  囚心锁ツ
    2020-11-29 01:56

    Should we avoid do-notation in any case?

    I'd say definitely no. For me, the most important criterion in such cases is to make the code as much readable and understandable as possible. The do-notation was introduced to make monadic code more understandable, and this is what matters. Sure, in many cases, using Applicative point-free notation is very nice, for example, instead of

    do
        f <- [(+1), (*7)]
        i <- [1..5]
        return $ f i
    

    we'd write just [(+1), (*7)] <*> [1..5].

    But there are many examples where not using the do-notation will make code very unreadable. Consider this example:

    nameDo :: IO ()
    nameDo = do putStr "What is your first name? "
                first <- getLine
                putStr "And your last name? "
                last <- getLine
                let full = first++" "++last
                putStrLn ("Pleased to meet you, "++full++"!")
    

    here it's quite clear what's happening and how the IO actions are sequenced. A do-free notation looks like

    name :: IO ()
    name = putStr "What is your first name? " >>
           getLine >>= f
           where
           f first = putStr "And your last name? " >>
                     getLine >>= g
                     where
                     g last = putStrLn ("Pleased to meet you, "++full++"!")
                              where
                              full = first++" "++last
    

    or like

    nameLambda :: IO ()
    nameLambda = putStr "What is your first name? " >>
                 getLine >>=
                 \first -> putStr "And your last name? " >>
                 getLine >>=
                 \last -> let full = first++" "++last
                              in  putStrLn ("Pleased to meet you, "++full++"!")
    

    which are both much less readable. Certainly, here the do-notation is much more preferable here.

    If you want to avoid using do, try structuring your code into many small functions. This is a good habit anyway, and you can reduce your do block to contain only 2-3 lines, which can be then replaced nicely by >>=, <$>,<*>` etc. For example, the above could be rewritten as

    name = getName >>= welcome
      where
        ask :: String -> IO String
        ask s = putStr s >> getLine
    
        join :: [String] -> String
        join  = concat . intersperse " "
    
        getName :: IO String
        getName  = join <$> traverse ask ["What is your first name? ",
                                          "And your last name? "]
    
        welcome :: String -> IO ()
        welcome full = putStrLn ("Pleased to meet you, "++full++"!")
    

    It's a bit longer, and maybe a bit less understandable to Haskell beginners (due to intersperse, concat and traverse), but in many cases those new, small functions can be reused in other places of your code, which will make it more structured and composable.


    I'd say the situation is very similar to whether to use the point-free notation or not. In many many cases (like in the top-most example [(+1), (*7)] <*> [1..5]) the point-free notation is great, but if you try to convert a complicated expression, you will get results like

    f = ((ite . (<= 1)) `flip` 1) <*>
         (((+) . (f . (subtract 1))) <*> (f . (subtract 2)))
      where
        ite e x y = if e then x else y
    

    It'd take me quite a long time to understand it without running the code. [Spoiler below:]

    f x = if (x <= 1) then 1 else f (x-1) + f (x-2)


    Also, why do most tutorials teach IO with do?

    Because IO is exactly designed to mimic imperative computations with side-effects, and so sequencing them using do is very natural.

提交回复
热议问题