关于 Haskell 的数字, Bool, List, if语句
同系列传送门: haskell-basic
Haskell 是一门静态强类型语言, 我们接下来将学习一些基础类型
你可以在终端敲下 ghci
进入交互解释器, 去执行接下来的代码
(2 + 8) * 9 - 10 -- 80
5 / 2 -- 2.5
5 ^ 2 -- 25
5 ^ (2.0) -- error
5 ** (2.0) -- 25.0
加减乘除就不说了, 对于 ^
与 **
都是幂函数, 底数的类型随意
但 ^
接受的指数是个整数, **
则可以接受小数/整数 (类型方面之后再讲, 这里有点不准确)
以上的运算符号, 实际上都是函数
比如, 你可以键入 :t (+)
, 来得到 +
这个函数的类型:
(+) :: Num a => a -> a -> a
=>
后面的表示参数与返回值的类型
=>
前的一块是类型类(Typeclass), 起类型约束的作用, 相当于其他语言的 trait/interface
看不懂没关系, 之后会学, 跳过即可, 此处只是为了告诉你, 在 ghci
中, 如何查看类型而已
以上的东西, 实际上类似于:
function (+)<a: Num> (a, a, a) -> a { }
Haskell 中的小写字母相当于无约束的泛型, 啥都能匹配
Num 这个类型类了, 便起到一个约束的作用
同时, 由于加法函数的定义, "ABC" + 123
这样的表达式, 会直接报错
因为不满足传入参数的要求: 两个参数的类型必须相等, 因为它们都是a
True && False -- False
True || False -- True
not True -- False
你还可以用 ==
或 /=
来得到一个Bool值:
(注: 我的博客可能启用了连体字, 上面的等号是两个等号, 不等号是斜杠+等号)
1 == 0 -- False
2 + 3 == 1 + 4 -- True
"ABC" /= "AB" -- True
值得注意的是, 等号与不等号的左右, 两个值的类型必须相等
这是因为 Haskell 中的运算符都是函数, 已经定义好了类型
可以键入 :t (==)
查看其类型:
(==) :: Eq a => a -> a -> Bool
表示接受的两个参数, 其类型相同, 都是a
并且a类型必须具有相等性, 返回值是Bool类型
注意:
由于其定义, "ABC" == 123
这样的表达式, 会直接报错
原因与先前的加法函数同理
等到后面, 我们可以自己动手, 实现 &&
, ||
, not
这三个函数
甚至连 if 语句, 我们都可以自己写个函数来代替
if 语句在 Haskell 中仅仅是个语法糖而已
在 Haskell-Wiki 上, 甚至有个讨论, 正反双方辩论要不要抛弃 if-else
这样的语法糖
感兴趣的话, 可以自己去看下: 传送门
List(列表), 在 Haskell 中扮演着一个非常重要的角色
就像是其他语言的数组一样, 但却更加强大
像下面, 就创建了一个有十个元素的List:
[1,2,3,4,5,6,7,8,9,10]
上面的List, 类型为 Num a => [a]
a 实际上就是元素的类型, [a] 表示装着这种类型元素的List
虽然我想把常用的函数放到之后再讲, 但还是得先教你一个: take
你可以查看它的类型:
take :: Int -> [a] -> [a]
这表示, 它接受两个参数, 第一个表示要取前面多少个元素, 第二个是任意类型的List, 随后返回新的List
比如:
take 5 [1,2,3,4,5,6,7,8,9,10] -- [1,2,3,4,5]
这个函数你马上就会用到, 在处理无限元素的List时非常好用
回到刚才, 你可以一个个地输入元素, 创建List
不过这太麻烦了, 如果我想要一百个, 一千个连续的整数呢? 你可以像下面这样
[1..10]
[1..100]
[1..1000]
诸如 [m..n]
这样的形式, 会创建一个闭区间, 从m遍历到n
当然, 得先保证元素本身的类型, 是可以进行遍历的, 即元素本身是 Enum
这个类型类的成员
从 Rust, Scala, Java 等语言出发, 相当于实现了 Enum
这个 Trait/Interface
你甚至可以舍去右端, 写下 [1..]
这样的式子来表达1到无穷
你还可以生成 [Char], 比如 ['a'..'z']
将会生成 "abcdefghijklmnopqrstuvwxyz"
, 因为String类型等价于 [Char]
值得注意的是, 忽略右端点时, 生成的List可能无穷大, 也可能是有界的, 我们先假设元素的类型为a
当a也是 Bounded
类型类的成员时, 说明这种类型一定有边界, 比如 Int
, Char
等
不然的话, a将无界, 比如 [1..]
便是一个真正的, 从1到正无穷的List, 类型为 Num a => [a]
当你看到这里时, 可能会有个疑问: 1 的类型不应该是Int吗? 为什么会是 Num a => a
呢?
事实上, 这是 Haskell 中为数不多的隐式行为
任何整数的字面量, 为了运算方便, 都把它们看作实现了Num类型类的类型, 以便于向下转型
举个例子, 你使用了Integer(无限精度的Int) 与 整数字面量:
a = 100::Integer -- 100
a + 1 -- 101
a + 1::Int -- error
我们先声明了a, 分别与整数字面量, Int类型数字相加
对于第一个加法, 1是个字面量, 是Num, 编译器可以自动推导出1应该也是Integer类型的
根据 (+)函数 的定义, 同时也因为Integer是Num这个Typeclass的成员
回到先前的 [1..]
, 1是Num, 当 List 产生的数字超越 Int 的最大值时, 不会溢出, 而是转成 Integer 以满足需求
先前我们讲到, 你除了傻乎乎地去一个个地填写List的元素, 还可以省略右端点
同时, 取决于元素的类型, 会产生无限或有界的List
但是, 比如 [1..5] 会生成 [1,2,3,4,5]
, 默认情况下的步长是1
如果你想调整步长的话, 可以像下面这样:
[1,3..] -- 步长是2, [1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33..]
[5,10..] -- 步长是5, [5,10,15,20,25,30,35,40,45,50,55,60,65,70..]
你还可以设置小数的步长, 比如:
[1.0, 1.5..] -- 步长是0.5, [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0..]
哇塞, 真是个聪明的编译器呢, 当然, 编译器有时候也不会如你所愿的 :)
Haskell 中的 List, 还有一个强大的地方, 那就是 List Comprehension (列表推导)
听着挺懵的, 来个例子吧, 以下表达式将创建一个无限级List, 元素是2的倍数
[x | x <- [1..], mod x 2 == 0] -- [2,4,6,8,10,12,14,16,18, 20..]
你会发现, 上面表示这样一个List的方法, 和高一学的集合的表示法非常相近 (因为就是这样抄来的啊)
希望你还没忘记高一学习的, 表示集合的方法之一, 列举法: {x | n ∈ Z, x = 2n}
, 表示了偶数集
Haskell 中的 List Comprehension
也有这么个杠:
[ x | x <- xs, bool_expr1, bool_expr2 ]
|
分割组成的表达式, 与后面的推导bool_expr
表示要满足的条件, 只有当 x 代入这个表达式, 满足条件才行, 可以有零个或多个可能你还是有点懵? 没事, 多看点例子, 直觉上明白就可以了
[x + y | x <- [1..3], y <- [1..3]] -- [2,3,4,3,4,5,4,5,6]
[x | x <- [1..100], x `mod` 2 == 0, x `mod` 3 == 0] -- [6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96]
其实, 就相当于 多重for循环
+ 最后一重for循环中的零个或多个if语句
随后利用满足条件的值, 创建新的表达式, 放进新的集合并返回:
[x + y | x <- [1..3], y <- [1..3]]
, 相当于:let new_array
for x in [1..3]
for y in [1..3]
(x + y) -> new_array
[x | x <- [1..100], x
mod2 == 0, x
mod 3 == 0]
, 相当于:let new_array
for x in [1..100]
if x `mod` 2 == 0 && x `mod` 3 == 0
(x) -> new_array
因此, 你可以对比下下面两种式子:
[ x * y | x <- [1..10], y <- [1..x] ]
[ x * y | x <- [1..y], y <- [1..10] ]
第一种合法, 但第二种不合法, 把它们理解为 for 循环就明白原因了: 变量 y 要提前声明
好了, 关于 List 暂时先到这吧, 其实后面还有很多关于 List 的, 毕竟它太重要了
先前说道, if语句仅仅是个语法糖, 但我们还是有必要学下这个语法的
顺便找个机会, 教下如何将代码写进文件并编译, 脱离 ghci
毕竟 Haskell 比较特殊, 为了函数的 纯度 , 特意搞了一堆东西
再不讲恐怕之后都讲不了呢, 所以我打算强硬点穿插着讲 (捂嘴笑)
开始吧!
首先, 让我们新建一个文件, 命名为 demo.hs
BMI, Body Mass Index (身体质量指数)
它与体内脂肪总量密切相关,常用来衡量人体胖瘦程度、是否健康
让我们来编写一个程序, 根据输入的bmi判断胖与瘦吧
你可以这样运行以 .hs 结尾的文件:
demo.hs
所在的目录下, 输入 runghc demo.hs
, 不会留下目标文件ghc demo.hs
, 留下目标文件 ./demo
main = do
putStrLn "Please input your BMI:"
bmi <- readLn
if bmi > 25
then putStrLn "Fat!!!"
else putStrLn "Thin!!!"
先别在意那个 do
与 <-
是啥, 无视即可, 反正这段代码你应该也看得懂:
程序会读取输入, 作为 bmi
的值, 随后根据大小, 判断是胖是瘦
当然, 如果你使用LSP, 代码格式化之后, 你可以得到下面这一段:
main = do
putStrLn "Please input your BMI:"
bmi <- readLn
if bmi > 25
then putStrLn "Fat!!!"
else putStrLn "Thin!!!"
也蛮美观的, 但我更喜欢压行 :)
其实在 Haskell 中根本不存在多重if这种玩意儿, 你往下看就会懂...
先前的 单if 实在不够, 没有区分输入不对劲的情况
而且只是简单地判断了胖瘦, 让我们再加一个判断是否健康的情况吧
main = do
putStrLn "Please input your BMI:"
bmi <- readLn
if bmi < 0
then putStrLn "What?"
else if bmi > 25
then putStrLn "Fat!!!"
else if bmi <= 25 && bmi <= 18.5
then putStrLn "Healthy!!!"
else
putStrLn "Thin!!!"
哇, 看着好美, 好熟悉!
所以你为什么说没有多重if呢?
别急, 格式化之后:
main = do
putStrLn "Please input your BMI:"
bmi <- readLn
if bmi < 0
then putStrLn "What?"
else
if bmi > 25
then putStrLn "Fat!!!"
else
if bmi <= 25 && bmi < 18.5
then putStrLn "12"
else putStrLn "Thin!!!"
懂了吧? 它只是 else 中再套一个 if-else 而已
而且就连 if-else, 我们都可以用自己定义的函数取代掉 (之后会讲)
就算我们不取代 if-else, 它的使用率也很低, 模式匹配更加美观, 也更强大 (Guard也是之后会讲)
这节就到这, 看辉夜3的最后一集去了~~