Haskell - 如何避免与 IO 混为一谈
Haskell - how to avoid messing pure with IO
我正在 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]
。这也涉及到variant with function,因为它也可以产生IO。
总而言之,哪种解决方案更好 - 具有生成函数还是预生成数据?
提前致谢。
一个选择是采用 流 而不是列表。如果生成值涉及执行 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
读为 "a way to use operations in m
to produce successive values of type a
and finally a result of type r
"。此 algo
函数不承诺任何特定的数据生成方式;它们可以纯粹地创建:
algo a (S.each [these, are, my, elements])
或在IO
、
内
algo a $ S.takeWhile (> 3) (S.readLn :: Stream (Of Int) IO ())
或使用随机 monad,或任何你喜欢的。
相比之下,我将采用与相反的方法。
只需使用列表。
考虑你的第一个例子:
algo :: a -> ??? -> [a]
algo n dgf = map (\x -> x * n) $ dgf
你问"How to write function header correctly, as dgf can be any function with any number of parameters?"
嗯,一种方法是使用 uncurrying。
通常,Haskell 函数是柯里化的。如果我们有一个像
这样的函数
add :: Int -> Int -> Int
add x y = x + y
并且我们想要一个将其输入加 2 的函数,我们可以使用 add 2
。
>>> map (add 2) [1..10]
[3,4,5,6,7,8,9,10,11,12]
因为add
实际上不是一个有两个参数的函数,
它是一个参数的函数,returns 一个参数的函数。
我们可以在上面的 add 的参数中添加括号以使其更清楚:
add :: Int -> (Int -> Int)
在Haskell中,所有函数都是单参数函数。
然而,我们也可以走另一条路——uncurry
一个函数
returns 一个函数来获取一个接受一对的函数:
>>> :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
,尽管可能没有
一个可爱的库函数为我们做这件事。
考虑到这一点,我们可以通过将一个未柯里化的传递给它来实现 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
仅使用 f
和 t
一次。那么为什么不直接将列表传递给它呢?
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'
的参数
直到它被实际使用,所以我们不必担心 "wasting" 时间计算
实际不会使用的参数:
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
对 monadic 值进行操作,不如采用流
dfeuer 解释的基于方法。或者你可以只使用一个列表。
仅仅因为你在 monad 中,并不意味着你的价值观突然变得严格。
例如,想要无限的随机数列表?没问题。
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)
我正在 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]
。这也涉及到variant with function,因为它也可以产生IO。
总而言之,哪种解决方案更好 - 具有生成函数还是预生成数据?
提前致谢。
一个选择是采用 流 而不是列表。如果生成值涉及执行 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
读为 "a way to use operations in m
to produce successive values of type a
and finally a result of type r
"。此 algo
函数不承诺任何特定的数据生成方式;它们可以纯粹地创建:
algo a (S.each [these, are, my, elements])
或在IO
、
algo a $ S.takeWhile (> 3) (S.readLn :: Stream (Of Int) IO ())
或使用随机 monad,或任何你喜欢的。
相比之下,我将采用与
只需使用列表。
考虑你的第一个例子:
algo :: a -> ??? -> [a]
algo n dgf = map (\x -> x * n) $ dgf
你问"How to write function header correctly, as dgf can be any function with any number of parameters?"
嗯,一种方法是使用 uncurrying。
通常,Haskell 函数是柯里化的。如果我们有一个像
这样的函数add :: Int -> Int -> Int
add x y = x + y
并且我们想要一个将其输入加 2 的函数,我们可以使用 add 2
。
>>> map (add 2) [1..10]
[3,4,5,6,7,8,9,10,11,12]
因为add
实际上不是一个有两个参数的函数,
它是一个参数的函数,returns 一个参数的函数。
我们可以在上面的 add 的参数中添加括号以使其更清楚:
add :: Int -> (Int -> Int)
在Haskell中,所有函数都是单参数函数。
然而,我们也可以走另一条路——uncurry
一个函数
returns 一个函数来获取一个接受一对的函数:
>>> :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
,尽管可能没有
一个可爱的库函数为我们做这件事。
考虑到这一点,我们可以通过将一个未柯里化的传递给它来实现 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
仅使用 f
和 t
一次。那么为什么不直接将列表传递给它呢?
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'
的参数
直到它被实际使用,所以我们不必担心 "wasting" 时间计算
实际不会使用的参数:
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
对 monadic 值进行操作,不如采用流
dfeuer 解释的基于方法。或者你可以只使用一个列表。
仅仅因为你在 monad 中,并不意味着你的价值观突然变得严格。
例如,想要无限的随机数列表?没问题。
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)