《Haskell趣学指南》—— 第1章,第1.6节元组

本节书摘来自异步社区《Haskell趣学指南》一书中的第1章,第1.6节元组,作者 【斯洛文尼亚】Miran Lipovaca,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.6 元组
元组(tuple)允许我们将多个异构的值组合成为一个单一的值。
从某种意义上讲,元组很像列表。但它们却有着本质的不同。首先就像前面所说,元组是异构的,这表示单个元组可以含有多种类型的元素。其次,元组的长度固定,在将元素存入元组的同时,必须明确元素的数目。

元组由括号括起,其中的项由逗号隔开。

ghci> (1, 3) 
(1,3) 
ghci> (3, 'a', "hello")
(3,'a',"hello")
ghci> (50, 50.4, "hello", 'b') 
(50,50.4,"hello",'b') 

1.6.1 使用元组
为理解元组的用途,我们可以思考一下Haskell中二维向量的表示方法。使用列表是可以的,按照[x, y]的形式,它倒也工作良好。若要将一组向量置于一个列表中来表示二维坐标系中的平面图形又该怎样?我们可以写个列表的列表,像这样:[[1,2],[8,11],[4,5]]。

但是,如果遇到[[1,2],[8,11,5],[4,5]]这样的列表并把它当做向量列表来使用,这种方法就有问题了。它并不是合法的向量列表,却是一个合法的列表的列表,毕竟其中元素的类型都相同(数值的列表组成的列表)。有这种情况存在,编写处理向量与图形的函数将复杂得多。

然而长度为2的元组(也称作序对,pair)与长度为3的元组(也称作三元组,triple)被视为不同的类型。这便意味着一个包含一组序对的列表不能再加入一个三元组。基于这个性质,使用元组来表示向量无疑更加合适。

要将原先的向量改为用元组表示,可以把里面的方括号改成括号,像这样[(1, 2), (8, 11), (4, 5)]。如果不小心将二元组与三元组混到了一起,就会报出以下的错误:

ghci> [(1,2),(8,11,5),(4,5)] 
Couldn't match expected type `(t, t1)'
against inferred type `(t2, t3, t4)'
In the expression: (8, 11, 5) 
In the expression: [(1, 2), (8, 11, 5), (4, 5)] 
In the definition of `it': it = [(1, 2), (8, 11, 5), (4, 5)] 

同样,即使两个元组的长度相同,但其中的元素的类型不一样,Haskell也会将它们视为不同的类型。比如[(1,2),("one",2)]这样的列表就有问题,因为其中的第一个元组是一对数,而第二个元组却成了一个字符串和一个数。

元组可以方便地用来表示一组数据的关联关系。比如,我们要表示一个人的姓名与年龄,可以使用这样的三元组:("Christopher", "Walken", 55)。

需要记住,元组是固定大小的——使用元组时应事先了解它里面含有多少项。乍一接触可能会觉得挺死板,不过每个不同长度的元组都是独立的类型,这也就意味着你无法写一个通用的函数为它追加元素。而只能单独写一个将元素与二元组构成三元组的函数、将元素与三元组构成四元组的函数,以及将元素与四元组构成五元组的函数,依此类推。

同列表相同,只要其中的项是可比较的,元组也可以比较大小,只是不可以像比较不同长度的列表那样比较不同长度的元组。

可以有单元素的列表,但元组不行。可以这样想:单元素的元组的性质与它里面包含的元素本身几乎一模一样,那么徒增一个新的类型又有什么用处呢?

1.6.2 使用序对
在Haskell中,将数据保存在序对里十分常见。对此,Haskell内置了许多有用的函数来处理序对。下面给出的是其中的两个函数。

fst取一个序对作为参数,返回其首项。
ghci> fst (8,11) 
8
ghci> fst ("Wow", False) 
"Wow"
snd取一个序对作为参数,返回其尾项。
ghci> snd (8,11) 
11
ghci> snd ("Wow", False) 
False

注意:
这两个函数仅对序对有效,而不能应用于三元组、四元组和五元组之上。稍后,我们将过一遍从元组中取数据的所有方式。
zip函数可以用来酷酷地生成一组序对的列表。它取两个列表作为参数,然后将它们交叉配对,形成一组序对。它的外表很简单,不过当你需要同时遍历两个列表时,就可以体会到它的实用。比如:

ghci> zip [1,2,3,4,5] [5,5,5,5,5] 
[(1,5),(2,5),(3,5),(4,5),(5,5)] 
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]

注意,由于序对中可以含有不同的类型,zip函数也可以将不同类型的序对组合在一起。不过,若要组合两个不同长度的列表会怎么样?

ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
[(5,"im"),(3,"a"),(2,"turtle")]

可见,较长的列表会在中间断开,至较短的列表结束为止,余下的部分就直接忽略掉了。而且,由于Haskell是惰性的,我们也可以使用zip组合有限的和无限的列表:

ghci> zip [1..] ["apple", "orange", "cherry", "mango"]
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

1.6.3 找直角三角形
接下来思考一道同时用到元组与列表推导式的题目,使用Haskell来找到所有满足下列条件的直角三角形:

三边长度皆为整数;
三边长度皆小于等于10;
周长(三边之和)为24的直角三角形。

如果三角形中有一个角是直角,那它就是一个直角三角形。直角三角形有一条重要的性质,那就是直角的两条边的平方和等于对边的平方。如图所示,两条直角边分别标记为a和b,直角的对边标记为c。其中c也被称作斜边。

首先,把所有三个元素都小于等于10的元组都列出来:

ghci> let triples = [ (a,b,c) | c <- [1..10], a <- [1..10], b <- [1..10] ] 

我们在三个列表中取值,并且左侧的输出部分将它们组合为一个三元组。随后在GHCi下边执行triples,可得到一个含有1000个元素的列表,就不在这里列出了。

我们接下来给它添加一个过滤条件,使其符合勾股定理(a2+b2=c2)。同时也考虑上a边要短于斜边(c边),b边要短于a边情况:

ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], a <- [1..c], b <- [1..a], 
a^2 + b^2 == c^2] 

注意,在这里我们限制了求解的区间,不再检查多余的三元组,比如b边比斜边长的情形(直角三角形中,斜边总比直角边长),同时假定b边总不长于a边。这对结果是没有影响的,即使忽略掉所有使a^2+b^2=c^2且b>a的(a, b, c),还有(b, a, c)在——它们实际上同一个三角形,只是直角边的顺序相反罢了。否则,我们得到的列表将无端多出一半重复的三角形。

注意:
在GHCi中,不允许将定义与表达式拆为多行。不过在本书中,因为版面的原因,我们偶尔不得不将一行代码折行。(不然这本书将特别特别宽,宽到放不到普通的书架上了——读者也就只有买个大书架才行呢。)
已经差不多了。最后修改函数,告诉它只要周长为24的三角形。

ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], a <- [1..c], b <- [1..a],
a^2 + b^2 == c^2, a+b+c == 24] 
ghci> rightTriangles'
[(6,8,10)] 

得到正确结果!这便是函数式编程的一般思路:先取一个初始的集合并将其变形,随后持续地利用过滤条件缩小计算范围,最终取得一个(或多个)最终解。

1 Cons来自于Lisp,意为构建(construct)一个序对,而序对正是Lisp中列表的基本节点。—译者注
2 range为作者的双关语,它既有“区间”的意思,亦有“牧场”的意思。因此在这里的插图是得州牛仔。—译者注

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值