【Haskell】一个没有循环的世界

在Haskell中写代码最不习惯的一个地方大概就是遇到循环时无从表达。在Haskell中,既没有for循环,也没有while循环。以至于在写代码时,你的很多想法没法用代码表达。这感觉大概就像一个三岁小孩面对一只螃蟹,嗯……(陷入沉思)

为什么Haskell没有循环?

我们一直在强调Haskell中的一个基本原则:一切皆是表达式。循环和表达式似乎怎么也联系不起来,循环本身是没有结果的,而且你很难去界定循环的结果应该是什么。其次,除了不需要结束的死循环,循环通常伴随着状态变化,而改变和Haskell中的一切皆不可变是相冲突的。所以Haskell中并没有循环的语法,这让习惯了命令式编程的我们感到抓心饶肝。本期文章我们就来看看如何在Haskell中去处理循环类问题。

递归

在命令式编程中,我们经常会做一种优化,把递归转化成循环方式。对于基于栈的编程语言来说,递归太费栈了。然而在Haskell中,递归是我们的基本思考模式。在某些问题上,递归要比循环更加直观。虽然Haskell也运行在冯诺依曼机器上,但是我们不用担心递归的问题,因为Haskell的编译器足够"聪明"。

递归和循环是可以相互转化的,在循环问题中,有一类是经典递归问题,比如汉诺塔和斐波拉契数列算法等,对于这类问题,它们的递归特性十分明显,直接递归就完事了,这里我们看一个快速排序和八皇后的问题。

  • 快速排序
qsort [] = []
qsort (x:xs) = let left = (qsort . filter (<=x)) xs
                   right = (qsort . filter (>x)) xs
               in left ++ [x] ++ right
  • 八皇后
safe _ [] _ = True
safe a (x:xs) n = a /= x && a /= x + n && a /= x - n && safe a xs (n+1)

queens 0 = [[]]
queens n = [x : y | y <- queens (n-1), x <- [1..8], safe x y 1]

八皇后问题
八皇后问题是要求在一个 8x8 的棋盘内摆上8枚棋子,要求任何两个棋子不能在同一条横线、竖线和对角线上。按递归的思路,假设我们已经在前 n-1 行成功摆上棋子,现在要在第 n 行摆上棋子。我们只需要在第 n 行的每一列上试图摆上一颗棋子,如果能符合要求,那我们就会得到摆放前 n 行的一个解,要得到第 n 行的所有解,只需要把第 n 行的每一列都摆一下,把所有符合要求的结果都放入集合就可以了。同样,第 n-1 行的所有可能解也是这么来的。

对于经典循环类问题,也可以转化为递归算法,但通常不一定是最好的做法,下一节单子抽象中我们会看到这类问题的解法。此处仅以一个非常无聊的示例作为示例展示。

boring 0 = return ()
boring n = do 
    boring (n-1)
    putStrLn $ "无聊x" ++ show n

确实够无聊的。还有千万不要输入负数,你的栈会爆炸的。

单子抽象

单子的一个重要作用是可以组合计算,循环也是一种控制结构,用来重复执行某个计算。重复执行某个计算,也可以抽象成组合n个同样的计算,这就是单子的威力。甚至通过单子抽象,我们也能方便的解决某些递归问题。

我们可以将循环类问题分为单纯循环和基于数据结构的循环。数据结构基本上就是线性表,虽然也有基于图和树的循环算法,但最终都是转化为了基于线性表的循环。单纯循环不依赖与数据结构,只需要指定循环次数。

基于数据结构的循环

由于Haskell的强大抽象能力,Haskell中基于数据结构的循环不止限于列表。不过列表作为最常用的数据结构,我们还是以它为例。Haskell本身提供了丰富且功能强大的函数来操作列表,它们也能解决部分循环问题。关于这些函数会在列表专题中介绍,这里我们只看几个基于单子抽象的结构控制函数。

接下来要介绍的函数都在Control.Monad模块,因此别忘了导入它。

sequence和sequence_

sequencesequence_严格来说是用来连接单子运算的,它们的区别是后者不会保留单子运算结果。以下所有成对的函数都是以_结尾的不会保留单子计算结果。

> :t sequence
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)
> :t sequence_
sequence_ :: (Foldable t, Monad m) => t (m a) -> m ()

一个简单的例子是通过sequence读取一个字符串列表,通过sequence_打印列表,因为对于打印,我们并不关心结果。

readNstring n = sequence $ take n $ repeat getLine

printNstring xs = sequence_ $ map print xs

mapM和mapM_

mapMmapM_可以将单子运算应用到列表,它们的类型如下。

> :t mapM
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
> :t mapM_
mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m ()

例如前一个例子中打印字符串列表用mapM_表示如下:

printNstring' :: Foldable t => t String -> IO ()
printNstring' = mapM_ putStrLn

实际上,mapM_就是sequence_ . map

forM和forM_

forMforM_mapMmapM_的区别仅仅是参数顺序不同,前者实际上就是flip mapMflip mamM_

> :t forM
forM :: (Traversable t, Monad m) => t a -> (a -> m b) -> m (t b)
> :t forM_
forM_ :: (Foldable t, Monad m) => t a -> (a -> m b) -> m ()

第一个例子中打印字符串列表的函数同样可以用forM_实现。

printNstring'' xs = forM_ xs putStrLn

这就有点Go语言循环的味道了。

var xs = []string{"1", "2"}
for _, s := range xs {
	println(s)
}

filterM

filterMfilter推广到了单子运算,类型如下。

> :t filterM
filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a]

foldM和foldM_

foldMfoldM_是把fold推广到了单子运算,类型如下。

> :t foldM
foldM
  :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
> :t foldM_
foldM_
  :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m ()

我们可以用它来求解N皇后问题。

queensN n = foldM placeQueen [] [1..n]
    where placeQueen xs _ = [x:xs | x <- [1..n], safe x xs 1]

单纯循环

replicateM和replicateM_

replicateMreplicateM_不再依赖数据结构,而是可以指定循环次数,有点像for i := 0; i < 10; i++ { }。比如第一个例子中读取字符串列表的函数也可以通过它来实现。

readNstring' n = replicateM n getLine 

forever

forever用于需要无限循环的场景,类似于for { }。比如我们可以做一个没有感情的复读机。

repeater = forever $ do
    a <- getLine
    putStrLn a

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值