【问题标题】:Haskell - how to avoid messing pure with IOHaskell - 如何避免与 IO 混淆
【发布时间】:2017-09-06 15:41:30
【问题描述】:

我在 haskell 上实现了一些算法。该算法需要生成一些数据。

我有一个以生成函数为参数的算法函数。例如,算法只是将输入数据乘以 n:

 algo :: a -> ??? -> [a]
 algo n dgf = map (\x -> x * n) $ dgf

dgf 用于生成数据。如何正确编写函数头,dgf 可以是任意参数的任意函数?

另一个变体不接受生成函数,而是接受已经生成的数据。

algo :: a -> [b] -> [a]
algo n d = (\x -> n*x) d

所以,现在让我们假设我正在使用stdGen 生成数据,它使用 IO。如何使函数更通用,以便它可以接受 IO 实例和普通值,例如 [1,2,3]。这也与带函数的变体有关,因为它也可以产生 IO。

总而言之,哪个解决方案更好 - 具有生成功能或预生成数据?

提前致谢。

【问题讨论】:

  • 是什么阻止您在IO [a] 上使用algo,例如algo n <$> generateDgf?
  • 将签名写成algo :: _会让GHC告诉你它为你的函数推断出的类型。
  • StdGen 实际上是纯函数,System.Random 中的大多数函数也是如此。 getStdGen 确实是不纯的,但是大多数随机函数只是使用了 RandomGen 类型类,它是纯的。由于您可以从不纯代码中调用纯函数,因此您只需要从不纯代码中启动您的算法,就像@Zeta 暗示的那样。由于入口点 (main) 总是不纯的,您可以从那里调用 getStdGen,然后将 StdGen 值传递给您的纯函数。

标签: algorithm haskell monads io-monad


【解决方案1】:

一种选择是采用 而不是列表。如果生成值涉及执行IO,并且可能有很多很多值,这通常是最好的方法。有几个包提供某种类型的流,但我将在此示例中使用 streaming 包。

import qualified Streaming.Prelude as S
import Streaming

algo :: Monad m => a -> Stream (Of a) m r -> Stream (Of a) m r
algo a = S.map (a +)

您可以将Stream (Of a) m r 理解为“一种使用m 中的操作来生成a 类型的连续值以及最终r 类型的结果的方法”。这个algo 函数不承诺任何特定的数据生成方式;它们可以纯粹地创建:

algo a (S.each [these, are, my, elements])

或在IO内,

algo a $ S.takeWhile (> 3) (S.readLn :: Stream (Of Int) IO ())

或者使用随机单子,或者任何你喜欢的东西。

【讨论】:

    【解决方案2】:

    作为对比,我将采取与dfeuer's answer相反的方法。

    只使用列表。

    考虑你的第一个例子:

    algo :: a -> ??? -> [a]
    algo n dgf = map (\x -> x * n) $ dgf
    

    您问“如何正确编写函数头,因为 dgf 可以是具有任意数量参数的任何函数?”

    嗯,一种方法是使用 uncurrying。

    通常,Haskell 函数是柯里化的。如果我们有这样的功能

    add :: Int -> Int -> Int
    add x y = x + y
    

    我们想要一个函数,可以在其输入中添加两个,我们可以使用 add 2

    >>> map (add 2) [1..10]
    [3,4,5,6,7,8,9,10,11,12]
    

    因为add 实际上并不是一个接受两个参数的函数, 它是一个参数的函数,它返回一个参数的函数。

    我们可以在上面的 add 参数中添加括号以使这一点更清楚:

    add :: Int -> (Int -> Int)
    

    在 Haskell 中,所有函数都是一个参数的函数。

    不过,我们也可以换一种方式——uncurry 一个函数 返回一个函数以获取一个接受一对的函数:

    >>> :t uncurry
    uncurry :: (a -> b -> c) -> (a, b) -> c
    >>> :t uncurry add
    uncurry add :: (Int, Int) -> Int
    

    这也很有用,比如我们想在一个列表中找到每一对的总和:

    >>> map (uncurry add) [ (1,2), (3,4), (5,6), (7,8), (9,10) ]
    [3,7,11,15,19]
    

    一般来说,我们可以取消任何a0-> a1 -> ... -> aN -> b 类型的函数 进入函数(a0, a1, ..., aN) -> b,尽管可能没有 一个可爱的库函数来为我们做这件事。

    考虑到这一点,我们可以通过传递一个 uncurried 来实现 algo 函数和值的元组:

    algo :: Num a => a -> (t -> [a]) -> t -> [a]
    algo n f t = map (\x -> x * n) $ f t
    

    然后使用匿名函数来取消我们的参数函数:

    >>> algo 2 (\(lo,hi) -> enumFromTo lo hi) (5, 10)
    [10,12,14,16,18,20]
    >>> algo 3 (\(a,b,c,d) -> zipWith (+) [a..b] [c..d]) (1, 5, 10, 14)
    [33,39,45,51,57]
    

    现在我们可以这样做,但我们不需要这样做。如上所述, algo 只使用一次ft。那么为什么不直接将列表传递给它呢?

    algo' :: Num a => a -> [a] -> [a]
    algo' n ns = map (\x -> x * n) ns
    

    它计算相同的结果:

    >>> algo' 2 $ (\(lo,hi) -> enumFromTo lo hi) (5, 10)
    [10,12,14,16,18,20]
    >>> algo' 2 $ enumFromTo 5 10
    [10,12,14,16,18,20]
    >>> algo' 3 $ (\(a,b,c,d) -> zipWith (+) [a..b] [c..d]) (1, 5, 10, 14)
    [33,39,45,51,57]
    >>> algo' 3 $ zipWith (+) [1..5] [10..14]
    [33,39,45,51,57]
    

    此外,由于 haskell 是非严格的,algo' 的参数不会被评估 直到它被实际使用,所以我们不必担心“浪费”时间计算 实际不会使用的参数:

    algo'' :: Num a => a -> [a] -> [a]
    algo'' n ns = [n,n,n,n]
    

    algo'' 不使用传递给它的列表,所以它从不强制,所以不管 计算用于计算它永远不会运行:

    >>> let isPrime n = n > 2 && null [ i | i <- [2..n-1], n `rem` i == 0 ]
    >>> :set +s
    >>> isPrime 10000019
    True
    (6.18 secs, 2,000,067,648 bytes)
    >>> algo'' 5 (filter isPrime [1..999999999999999])
    [5,5,5,5]
    (0.01 secs, 68,936 bytes)
    

    现在到您问题的第二部分 - 如果您的数据是在某个 monad 中生成的怎么办?

    与其说服algo 对一元值进行操作,不如采用流 dfeuer 解释的基于方法。或者你可以只使用一个列表。

    仅仅因为你在一个单子里,并不意味着你的价值观突然变得严格。

    例如,想要一个无限的随机数列表?没问题。

    newRandoms :: Num a -> IO [a]
    newRandoms = unfoldr (\g -> Just (random g)) <$> newStdGen
    

    现在我可以将它们传递给一些算法:

    >>> rints <- newRandoms :: IO [Int]
    (0.00 secs, 60,624 bytes)
    >>> algo'' 5 rints
    [5,5,5,5]
    (0.00 secs, 68,920 bytes)
    

    对于一个只从一两个文件中读取输入的小程序来说,没有问题 只需使用 readFile 和惰性 I/O 即可获取要操作的列表。

    例如

    >>> let grep pat lines = [ line | line <- lines, pat `isInfixOf` line ]
    >>> :set +s
    >>> dict <- lines <$> readFile "/usr/share/dict/words"
    (0.01 secs, 81,504 bytes)
    >>> grep "poop" dict
    ["apoop","epoophoron","nincompoop","nincompoopery","nincompoophood","nincompoopish","poop","pooped","poophyte","poophytic","whisterpoop"]
    (0.72 secs, 423,650,152 bytes)
    

    【讨论】:

    • 我不喜欢无限的随机数列表:它迫使我致力于纯 RNG。如果我后来(假设地)决定用hardware random number generator 做某种花式科学,我必须重写我的代码,或者用懒惰的IO 玩游戏。使用流,我可以随意推迟这个决定。我可以使用纯生成器开发和测试我的代码,然后在批准或其他任何情况下将其换成真实的。
    • 另一件事:使用随机数的列表,很容易不小心使用同一个数字两次。流也可以做到这一点,但我认为这有点困难;流 API 通常不鼓励这种事情。
    猜你喜欢
    • 2016-03-05
    • 1970-01-01
    • 1970-01-01
    • 2021-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-10
    相关资源
    最近更新 更多