gpt4 book ai didi

haskell - 添加 IO 时重构 Haskell

转载 作者:行者123 更新时间:2023-12-02 12:45:32 25 4
gpt4 key购买 nike

我担心 IO 的引入会对程序产生多大影响。假设我的程序深处的一个函数被更改为包含一些 IO;如何隔离此更改,而不必同时更改 IO 路径中的每个函数?

例如,在一个简化的示例中:

a :: String -> String
a s = (b s) ++ "!"

b :: String -> String
b s = '!':(fetch s)

fetch :: String -> String
fetch s = reverse s

main = putStrLn $ a "hello"

(这里的 fetch 更实际的是从静态 Map 中读取一个值作为其结果)但是,假设由于某些业务逻辑发生变化,我需要在某个数据库中查找 fetch 返回的值(我可以在此处通过调用 getLine 来举例说明):

fetch :: String -> IO String
fetch s = do
x <- getLine
return $ s ++ x

所以我的问题是,如何避免重写此链中的每个函数调用?

a :: String -> IO String
a s = fmap (\x -> x ++ "!") (b s)

b :: String -> IO String
b s = fmap (\x -> '!':x) (fetch s)

fetch :: String -> IO String
fetch s = do
x <- getLine
return $ s ++ x

main = a "hello" >>= putStrLn

我可以看到,如果函数本身不相互依赖,那么重构会简单得多。对于一个简单的例子来说这很好:

a :: String -> String
a s = s ++ "!"

b :: String -> String
b s = '!':s

fetch :: String -> IO String
fetch s = do
x <- getLine
return $ s ++ x

doit :: String -> IO String
doit s = fmap (a . b) (fetch s)

main = doit "hello" >>= putStrLn

但我不知道这在更复杂的程序中是否一定实用。到目前为止,我发现真正隔离像这样的 IO 添加的唯一方法是使用 unsafePerformIO,但是,就其名称而言,如果我能帮助的话,我不想这样做。还有其他方法可以隔离此更改吗?如果重构是实质性的,我会开始倾向于避免这样做(特别是在截止日期等情况下)。

感谢您的建议!

最佳答案

以下是我使用的一些方法。

  • 通过反转控制来减少对效果的依赖。(您在问题中描述的方法之一。)也就是说,在外部执行效果并传递结果(或具有这些结果的函数)部分应用)到纯代码中。不要使用 mainabfetch,而是使用 mainfetch然后mainab:

    a :: String -> String
    a f = b f ++ "!"

    b :: String -> String
    b f = '!' : f

    fetch :: String -> IO String
    fetch s = do
    x <- getLine
    return $ s ++ x

    main = do
    f <- fetch "hello"
    putStrLn $ a f

    对于更复杂的情况,您需要将参数线程化以通过多个级别执行这种“依赖项注入(inject)”,Reader/ReaderT 可以让您抽象在样板之上。

  • 编写您希望从一开始就需要单子(monad)风格效果的纯代码。(对单子(monad)的选择进行多态。)然后,如果您最终这样做了如果需要在该代码中添加效果,则无需更改实现,只需更改签名

    a :: (Monad m) => String -> m String
    a s = (++ "!") <$> b s

    b :: (Monad m) => String -> m String
    b s = ('!' :) <$> fetch s

    fetch :: (Monad m) => String -> m String
    fetch s = pure (reverse s)

    由于此代码适用于任何具有 Monad 实例(或者实际上只是 Applicative)的 m,因此您可以直接在 IO,或纯粹使用“虚拟”单子(monad)Identity:

    main = putStrLn =<< a "hello"

    main = putStrLn $ runIdentity $ a "hello"

    然后,当您需要更多效果时,您可以使用“mtl style”(如 @dfeuer 的答案所述)根据需要启用效果,或者如果您使用相同的 monad到处都是堆栈,只需将 m 替换为该具体类型,例如:

    newtype Fetch a = Fetch { unFetch :: IO a }
    deriving (Applicative, Functor, Monad, MonadIO)

    a :: String -> Fetch String
    a s = pure (b s ++ "!")

    b :: String -> Fetch String
    b s = ('!' :) <$> fetch s

    fetch :: String -> Fetch String
    fetch s = do
    x <- liftIO getLine
    return $ s ++ x

    main = putStrLn =<< unFetch (a "hello")

    mtl 风格的优点是您可以拥有多种不同的效果实现。这使得测试和模拟之类的事情变得容易,因为您可以重用逻辑,但使用不同的“处理程序”运行它以进行生产和测试。事实上,您可以使用代数效果库(例如 freer-effects)获得更大的灵 active (以牺牲一些运行时性能为代价)。 ,这不仅可以让调用者更改每个效果的处理方式,还可以更改处理效果的顺序。

  • 卷起袖子进行重构。编译器会告诉您任何需要更新的地方。经过足够多次的这样做后,您自然会意识到何时编写需要稍后进行重构的代码,因此您将从一开始就考虑效果,而不会遇到问题。

您对unsafePerformIO的怀疑是完全正确的!它不仅不安全,因为它破坏了引用透明度,它不安全,因为它还会破坏类型内存并发安全性 - 您可以使用它可以将任何类型强制为任何其他类型,导致段错误,或者导致通常不可能发生的死锁和并发错误。您告诉编译器某些代码是纯代码,因此它将假设它可以完成对纯代码所做的所有转换,例如复制、重新排序,甚至删除它,这可能会完全改变您的代码的正确性和性能。代码。

unsafePerformIO 的主要合法用例是使用 FFI 包装外部代码(您知道是纯代码),或者进行特定于 GHC 的性能黑客;否则请远离它,因为它并不意味着普通代码的“逃生舱口”。

关于haskell - 添加 IO 时重构 Haskell,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51201592/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com