有没有更可读的方法来重写这个纯函数来使用Writer Monad?

时间:2018-02-12 19:31:46

标签: haskell monads

我使用列表推导编写了一个递归算法来执行递归。我认为我的代码清晰易读,所产生的结果是正确的。

但是,我发现很难理解我的代码在某些输入上的性能。我认为使用Writer monad将一些日志记录放入我的代码中会很有用。

我发现将非monadic代码转换为monadic非常困难。最终我得到它编译并正确运行,但monadic代码比原始代码更难理解。

原始问题太复杂了,无法解释,所以我写了一个玩具示例,显示非monadic和monadic方法,但实际上没有计算任何有用的东西!

所以我的问题是:有没有更好的方法来编写函数fMonadic,这样它更具可读性?

import Control.Monad (forM)
import Control.Monad.Writer (Writer, runWriter, tell)

fun :: Int -> [[Int]] -> [[Int]]
fun a b = map (map (a +)) b

fNonMonadic :: [[Int]] -> [[Int]]
fNonMonadic [] = [[]]
fNonMonadic (first : rest) =
    [ first ++ s
    | e <- first
    , s <- fNonMonadic $ fun e rest]

fMonadic :: [[Int]] -> Writer [String] [[Int]]
fMonadic [] = do
    tell ["base case"]
    return [[]]
fMonadic (first : rest) =
    fmap concat . forM first $ \ e -> do
        tell ["recursive case " ++ show e]
        fmap (map (first ++)) $ fMonadic $ fun e rest

main = do
    let arg = [[0, 1], [20, 30], [400, 500]]
    print $ fNonMonadic arg
    let (a, b) = runWriter $ fMonadic arg
    print a
    mapM_ putStrLn b

2 个答案:

答案 0 :(得分:6)

装备纯Haskell函数通常很尴尬,这些函数以代数,高度分支的树方式构造,具有日志特征,例如日志记录,这需要更“必要”的结构。然而,有时使用monadic组合器编写甚至纯粹的计算实际上是很自然的,而你的实际上是其中之一。也就是说,fNonMonadic核心的列表理解已基本上使用 list monad ;它可以写成:

type ListM = []   -- Just to distinguish where I use list as a monad

fNonMonadic :: [[Int]] -> ListM [Int]
fNonMonadic [] = return []
fNonMonadic (first : rest) = do
    e <- first
    s <- fNonMonadic $ fun e rest
    return $ first ++ s

从此开始,通过将编写器功能用作 monad转换器堆栈的基础,可以更轻松地添加编写器功能。然后必须在变换器形状中使用该列表:

import Control.Monad.Trans.List

fMonTrafo :: [[Int]] -> ListT (Writer [String]) [Int]
fMonTrafo [] = do
    lift $ tell ["base case"]
    return []
fMonTrafo (first : rest) = do
    e <- ListT $ pure first
    lift $ tell ["recursive case " ++ show e]
    s <- fMonTrafo $ fun e rest
    return $ first ++ s

您可能会注意到ListT的文档警告基本monad应该是可交换的Writer实际上不是 - 日志条目的顺序可能会混乱起来。我不知道这是否重要。如果是,请查看the alternative implementation suggested by Daniel Wagner

答案 1 :(得分:2)

我查看了Control.Monad.Trans.List的几个替代方案,很快就从Volkov的list-t包中找到了模块ListT。

这给出了与我丑陋的fMonadic函数相同的结果,但代码更易读。它也可以正常工作,并导致可读的代码,在我想解决的实际问题中。

在真正的问题中,基于ListT的代码比丑陋的代码运行得稍慢,但差别不足以解决问题。

再次感谢leftaroundabout对此的帮助。

作为参考,这里是玩具示例的修订版本,以三种不同的方式进行计算并显示答案是相同的:

import Control.Monad (forM)
import ListT (ListT, fromFoldable, toList)
import Control.Monad.Writer (Writer, lift, runWriter, tell)

fun :: Int -> [[Int]] -> [[Int]]
fun a b = map (map (a +)) b

fNonMonadic :: [[Int]] -> [[Int]]
fNonMonadic [] = [[]]
fNonMonadic (first : rest) = do
    e <- first
    s <- fNonMonadic $ fun e rest
    return $ first ++ s
    -- The above do notation means the same as this list comprehension:
    -- [ first ++ s
    -- | e <- first
    -- , s <- fNonMonadic $ fun e rest]

fMonadic :: [[Int]] -> Writer [String] [[Int]]
fMonadic [] = do
    tell ["base case"]
    return [[]]
fMonadic (first : rest) =
    fmap concat . forM first $ \ e -> do
        tell ["recursive case " ++ show e]
        fmap (map (first ++)) $ fMonadic $ fun e rest

fMonTrafo :: [[Int]] -> ListT (Writer [String]) [Int]
fMonTrafo [] = do
    lift $ tell ["base case"]
    return []
fMonTrafo (first : rest) = do
    e <- fromFoldable first
    lift $ tell ["recursive case " ++ show e]
    s <- fMonTrafo $ fun e rest
    return $ first ++ s

main = do
    let arg = [[0, 1], [20, 30], [400, 500]]
    let x = fNonMonadic arg
    print x
    let (a, b) = runWriter $ fMonadic arg
    print a
    mapM_ putStrLn b
    let (c, d) = runWriter $ toList $ fMonTrafo arg
    print c
    mapM_ putStrLn d
    putStrLn $ if x == a then "fNonMonadic == fMonadic" else error ""
    putStrLn $ if x == c then "fNonMonadic == fMonTrafo" else error ""
    putStrLn $ if b == d then "fMonadic log == fMonTrafo log" else error ""