更新: 注意不要在 Haskell 中编写 Scheme 程序。累积参数在惯用的 Haskell 中很少出现,它使用惰性而不是尾递归。
例如,在 Haskell 中定义 count
的方式将类似于
count [] = 0
count (x:xs) = 1 + count xs
你可能会反对 length
的源代码看起来更像Scheme-ish
length :: [a] -> Int
length l = len l 0#
where
len :: [a] -> Int# -> Int
len [] a# = I# a#
len (_:xs) a# = len xs (a# +# 1#)
但这是低级、非惯用的代码。引用的genericLength
与上面的count
具有相同的结构,并且与length
相比具有更广泛的适用性。
genericLength :: (Num i) => [b] -> i
genericLength [] = 0
genericLength (_:l) = 1 + genericLength l
在 Haskell 中,“先做这个,然后做那个”被表达为纯代码中的函数组合。例如,要计算最长子列表的长度,我们将计算子列表的长度,然后计算这些长度的最大值。在 Haskell 中,这是这样表达的:
Prelude> :t maximum . map length
maximum . map length :: [[a]] -> Int
或者
Prelude> :m + Data.List
Prelude Data.List> :t maximum . map genericLength
maximum . map genericLength :: (Ord c, Num c) => [[b]] -> c
注意:懒惰性增加了细微差别,但总体观点仍然成立。
即使在
IO
内部的“命令式”代码中,
main :: IO ()
main = do
putStr "Hello "
purStrLn "world!"
函数组合是“在覆盖下”的,因为它具有与之相同的语义。
main :: IO ()
main = putStr "Hello " >>= \_ -> putStrLn "world!"
也许使用列表单子的示例会更加清晰。假设我们想要找到所有勾股三元组
(a,b,c)
,使得没有一个分量大于某个最大值
n。
import Control.Monad (guard)
triples :: Integer -> [(Integer,Integer,Integer)]
triples n = do
a <- [1 .. n]
b <- [a .. n]
c <- [b .. n]
guard $ a*a + b*b == c*c
return (a,b,c)
正如你所看到的,我们必须首先选择a
的候选值,然后是b
,依此类推。
编译器会将此代码转换为显式使用单子绑定组合器>>=
。
triples_bind :: Integer -> [(Integer,Integer,Integer)]
triples_bind n =
[1 .. n] >>= \a ->
[a .. n] >>= \b ->
[b .. n] >>= \c ->
(guard $ a*a + b*b == c*c) >>= \_ ->
return (a,b,c)
在列表单子里,
>>=
是
concatMap
,所以上述代码等同于:
triples_stacked :: Integer -> [(Integer,Integer,Integer)]
triples_stacked n =
concatMap (\a ->
concatMap (\b ->
concatMap (\c ->
concatMap (\_ ->
[(a,b,c)])
(guard $ a*a + b*b == c*c))
[b .. n])
[a .. n])
[1 .. n]
缩进显示结构,给人以赏心悦目的印象,因为它呈现了Haskell标志的组合λ和
>>=
。
表达相同语义的另一种方式是:
triples_cm n = concatMap step2 [1 .. n]
where step2 a = concatMap step3 [a .. n]
where step3 b = concatMap step4 [b .. n]
where step4 c = concatMap step5 (guard $ a*a + b*b == c*c)
where step5 _ = [(a,b,c)]
Haskell的模式匹配已经在幕后执行了
case
匹配。与其这样,
go 0 = 0
go i = case i of
1 -> 2
_ -> 3
go (i-1)
完成你开始的模式。
go 0 = 0
go 1 = go (2 - 1)
go _ = go (3 - 1)
请记住,Haskell 中的变量是不可变的:它们没有像 C 或 Java 中那样的破坏性更新。您只有一次机会定义变量的值,然后就必须使用它。您可以将 go
定义为:
go 0 = 0
go x = let i = case x of 1 -> 2
_ -> 3
in go (i - 1)
或者
go 0 = 0
go x | x == 1 = go (2 - 1)
| otherwise = go (3 - 1)
以上示例都经过了类型检查,但存在一个共同的大问题,即
go
仅在其参数为零时终止。你没有提供足够的信息来帮助我们解决这个问题。告诉我们你想做什么,我们可以针对你的情况提供具体的建议。
let ... in ...
?(不是顺序:在后续表达式中引入变量然后使用它们)。如果不知道你的简单程序尝试做什么,我们就无法帮助你解决问题。 - dave4420seq
的作用),但在这里不是。 - hammarlet
语句就足够了。你可以通过换行或分号将多个绑定引入到单个let语句中。此外,在非严格性的情况下,尾递归是毫无意义的;Haskell栈的工作方式与严格语言的栈完全不同。关于你的最后一个问题,我坚持我的原始答案:在Haskell中你组合。对于IO操作,这通常使用>>=
实现。对于纯函数,.
是规范的组合运算符。 - Dan Burton