我试着制作一个简单的计数器。然而,我的柜台并没有上升。在我看来好像每次都通过函数“inc”重新初始化,或者(n + 1)不起作用。我该如何最好地解决这个问题?
inc :: Int -> IO Int
inc n = return (n+1)
main :: IO ()
main = do
let c = 0
let f = 0
putStrLn "Starting..."
conn <- connect "192.168.35.62" 8081
time $
forM_ [0..10000] $ \i -> do
p <- ping conn "ping"
if p=="pong" then inc c
else inc f
printf "Roundtrips %d\n" (c::Int)
答案 0 :(得分:21)
虽然可变变量可以在Haskell中使用,如其他评论者所示,但它不是一个好的风格:在大多数情况下不应该使用变异。
inc
函数按值接受其参数,也就是说,它不会修改其参数。此外,let
声明的变量保留其初始值,因此您无法更改它们。
如果不能更改任何变量,您如何写?答案是:
幸运的是,您很少需要自己编写递归,因为大多数递归模式已经在标准库中。
在您的情况下,您需要执行多个IO操作并返回两个计数器的最终值。让我们从一个动作开始:
let tryOnePing (c, f) i = do
p <- ping conn "ping"
return $ if p == "pong" then (c+1, f) else (c, f+1)
这里我们声明一个带有2个参数的本地函数:计数器的当前值,打包在元组(Int, Int)
(其他语言中的结构)和当前迭代Int
中。该函数执行IO操作并返回计数器IO (Int, Int)
的修改值。这一切都在其类型中表示:
tryOnePing :: (Int, Int) -> Int -> IO (Int, Int)
ping
返回IO String
类型的值。要进行比较,您需要String
而不需要IO
。为此,您应该使用>>=
函数:
let tryOnePing (c, f) i = ping conn "ping" >>= \p -> {process the string somehow}
由于这种模式很常见,因此可以像这样编写
let tryOnePing (c, f) i = do
p <- ping conn "ping"
{process the string somehow}
但意思完全相同(编译器将do
表示法转换为>>=
的应用程序。
处理显示了一些更常见的模式:
if p == "pong" then (c+1, f) else (c, f+1)
此处if
不是命令式if
,而更像是其他语言中的三元condition ? value1 : value2
运算符。另请注意,我们的tryOnePing
函数接受(c,f)并返回(c+1, f)
或(c, f+1)
。我们使用元组,因为我们只需要使用2个计数器。如果计数器数量很多,我们需要声明一个结构类型并使用命名字段。
整个If构造的值是一个元组(Int,Int)。 ping
是一个IO操作,因此tryOnePing
也必须是IO操作。 return
函数不是强制性返回,而是将(Int, Int)
转换为IO (Int, Int)
的方法。
所以,因为我们有tryOnePing,我们需要编写一个循环来运行它1000次。你的forM_
不是一个好的选择:
_
表示它会抛出计数器的最终值而不是返回它此处您不需要forM_
,而是foldM
foldM tryOnePing (0, 0) [0 .. 10000]
foldM
执行由列表的每个元素参数化的IO操作,并在迭代之间传递一些状态,在我们的示例中是两个计数器。它接受初始状态,并返回最终状态。当然,当它执行IO操作时,它返回IO(Int,Int),因此我们需要使用>>=
再次提取它以显示:
foldM tryOnePing (0, 0) [0 .. 10000] >>= \(c, f) -> print (c, f)
在Haskell中,您可以执行所谓的“eta reduction”,即可以从函数声明的两侧删除相同的标识符。例如。 \foo -> bar foo
与bar
相同。所以在这种情况下>>=
你可以写:
foldM tryOnePing (0, 0) [0 .. 10000] >>= print
比do
符号短得多:
do
(c, f) <- foldM tryOnePing (0, 0) [0 .. 10000]
print (c, f)
另请注意,您不需要有两个计数器:如果您有3000个成功,那么您有7000个失败。所以代码变成了:
main = do
conn <- connect "192.168.35.62" 8081
let tryOnePing c i = do
p <- ping conn "ping"
return $ if p == "pong" then c+1 else c
c <- foldM tryOnePing 0 [0 .. 10000]
print (c, 10000 - c)
最后,在Haskell中,将IO操作与非IO代码分开是很好的。因此,最好将ping中的所有结果收集到列表中,然后计算成功的ping:
main = do
conn <- connect "192.168.35.62" 8081
let tryOnePing i = ping conn "ping"
pings <- mapM tryOnePing [0 .. 10000]
let c = length $ filter (\ping -> ping == "pong") pings
print (c, 10000 - c)
请注意,我们完全避免增加。
它写得更短,但需要更多的阅读和写作技巧。别担心,你很快就会学到这些技巧:
main = do
conn <- connect "192.168.35.62" 8081
c <- fmap (length . filter (== "pong")) $ mapM (const $ ping conn "ping") [0 .. 10000]
print (c, 10000 - c)
答案 1 :(得分:7)
在Haskell中,默认情况下数据是不可变的。这意味着c
中的inc c
始终为零。
要在Haskell中获取可变变量,您必须明确地要求它们 ,即使用IORefs。使用它们你可以写出类似的东西:
import Data.IORef
inc :: IORef Int -> IO ()
inc ref = modifyIORef ref (+1)
main :: IO ()
main = do
c <- newIORef 0
f <- newIORef 0
putStrLn "Starting..."
conn <- connect "192.168.35.62" 8081
time $
forM_ [0..10000] $ \i -> do
p <- ping conn "ping"
if p=="pong"
then inc c
else inc f
c' <- readIORef c
printf "Roundtrips %d\n" c'
答案 2 :(得分:5)
就像在IO
之外的代码中一样,您可以使用折叠将一系列计算串联起来。 foldM
在monad中运行,例如
main = do
conn <- connect "192.168.35.62" 8081
let tryOnePing (c, f) i = do
p <- ping conn "ping"
return $ if p == "pong" then (c+1, f) else (c, f+1)
(c, f) <- foldM tryOnePing (0, 0) [0 .. 10000]
print (c, f)
答案 3 :(得分:2)
变量在Haskell中是不可变的。当您致电inc f
时,它会返回您会立即忽略的0 + 1
值。 f
的值为0
,并且将一直保持这样。