1

我在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。

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

提前致谢。

4

2 回答 2

6

一种选择是采用而不是列表。如果生成值涉及执行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 ())

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

于 2017-09-06T17:29:19.237 回答
2

相比之下,我将采用与dfeuer 的答案相反的方法。

只需使用列表。

考虑你的第一个例子:

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 的函数 uncurry 到 function(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仅使用fandt一次。那么为什么不直接将它传递给列表呢?

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'因此在实际使用之前不会评估 to 的参数,因此我们不必担心“浪费”时间来计算实际上不会使用的参数:

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)
于 2017-09-07T01:26:20.620 回答