haskell-basic-p4~> 基础语法
<~~ 发表日期:2022-07-12 | 本文词数:2712 | 预计阅读时间:14分钟 ~~>

关于 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


Bool

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

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 ]   
  • | 分割组成的表达式, 与后面的推导
  • xs 表示一个List, 因为是 x 的复数, 也是尾巴后面加上s, 可以有一个或多个
  • 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, xmod 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语句

先前说道, if语句仅仅是个语法糖, 但我们还是有必要学下这个语法的
顺便找个机会, 教下如何将代码写进文件并编译, 脱离 ghci

毕竟 Haskell 比较特殊, 为了函数的 纯度 , 特意搞了一堆东西
再不讲恐怕之后都讲不了呢, 所以我打算强硬点穿插着讲 (捂嘴笑)

开始吧!

单if

首先, 让我们新建一个文件, 命名为 demo.hs

BMI, Body Mass Index (身体质量指数)
它与体内脂肪总量密切相关,常用来衡量人体胖瘦程度、是否健康

让我们来编写一个程序, 根据输入的bmi判断胖与瘦吧

你可以这样运行以 .hs 结尾的文件:

  • demo.hs 所在的目录下, 输入 runghc demo.hs, 不会留下目标文件
  • 输入 ghc demo.hs, 留下目标文件 ./demo
demo.hs
main = do
  putStrLn "Please input your BMI:"
  bmi <- readLn

  if  bmi > 25
    then putStrLn "Fat!!!"
  else putStrLn "Thin!!!"

先别在意那个 do<- 是啥, 无视即可, 反正这段代码你应该也看得懂:
程序会读取输入, 作为 bmi 的值, 随后根据大小, 判断是胖是瘦

当然, 如果你使用LSP, 代码格式化之后, 你可以得到下面这一段:

demo.hs
main = do
  putStrLn "Please input your BMI:"
  bmi <- readLn

  if bmi > 25
    then putStrLn "Fat!!!"
    else putStrLn "Thin!!!"

也蛮美观的, 但我更喜欢压行 :)

多if

其实在 Haskell 中根本不存在多重if这种玩意儿, 你往下看就会懂...

先前的 单if 实在不够, 没有区分输入不对劲的情况
而且只是简单地判断了胖瘦, 让我们再加一个判断是否健康的情况吧

demo.hs
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呢?

别急, 格式化之后:

demo.hs
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的最后一集去了~~