https://learnyoua.haskell.sg/zh-cn/ch02/ready-go
http://cnhaskell.com/chp/1.html

语法格式:https://www.haskell.org/onlinereport/haskell2010/haskellch10.html#x17-18000010.5

doc: Haskell Platform\8.6.5\dochttp://hackage.haskell.org/package/ghc-prim-0.5.3/docs/doc-index.html

缩进

  • where所在行的下一行代码的缩进记为A,那么此后的所有行的缩进都应该满足>A
  • 函数定义:函数f的定义第一行的缩进记为A,那么在对f的定义中,所有行的缩进都应该满足>A
  • 在一些情况下可以使用;来替代换行f :: Num a => a->a; f x = x,case x of Nothing -> 0; Just _ -> 1

列表生成式

是一个语法糖,实际是使用的递归和模式匹配来遍历列表

1
2
# 定义 迭代 过滤 的顺序与个数无要求
l = [<exp> | (<定义>|<迭代>|<过滤>,)?]
  • 定义let (<name> = <value>;)*,同一个let中不能出现相同的name,不同的定义可以出现相同的name,后面的覆盖前面的,没有in,不是let表达式,但是let是必须的。

let定义的值在子生成式中也可见

1
[x | let a = 1, x <- [ y | let y = a]] -- [1]
  • 迭代x <- xs,如果没有<迭代>那么只有一个元素,如果有<迭代>那么会以笛卡尔积的形式重复:
1
[ (a, b) | a <- [1..2], b <- [1..3] ] -- [(1,1),(1,2),(1,3),(2,1),(2,2),(2,3)]
  • 过滤x > 1,所有的过滤条件都为True值才会出现在结果中
1
l = [ x | let xs = [1,2,3], x <- xs, x > 1 ]
  • 取值时是从左到右的:
1
2
3
[ x | let xs = [1,2,3], let xs = [1,2,3,4], x <- xs, x > 1 ] -- [2,3,4]
[ x | let xs = [1,2,3], x <- xs, let xs = [], x > 1 ] -- [2,3]
[ x*2 | False, x <- [1,2] ] -- []

underscore _

doc: https://typeclasses.com/underscore

在某些地方单独出现时可以当作通配符或者hole

  • 模式中: 通配符,代表不关心是什么值

函数

本质上所有的函数都只有一个参数

函数内部可以使用外部量,但函数内的外部量会一直是函数定义时该量的值。也就是说Haskell中的量的作用域是词法作用域

函数定义/调用

1
2
3
4
5
<函数名> :: <类型描述>
<前缀函数名> <value>* = <函数体> // 函数体最后一个表达式的值就是返回值
<value> (`<中缀函数名>`|<符号>) <value>

<value> :: 见type/data章节部分

函数的定义类似一个命题,定义了某个表达式的归约规则:

1
2
3
4
5
6
7
8
9
flip' :: (a -> b -> c) -> b -> a -> c
-- flip' f 替换为g,然后将 g x y 替换为 f y x
flip' f = g
    where g x y = f y x

-- 更简单的方法
flip' :: (a -> b -> c) -> b -> a -> c  
-- 命题:flip' f x y 的值等于 f y x 的值
flip' f x y = f y x

符号函数是中缀的,非符号函数是前缀的

中缀函数添加括号后变成前缀函数,前缀函数添加反引号后变成中缀函数。(但是不允许双重操作,即非符合函数添加反引号后变成中缀函数,然后又添加括号想变回前缀函数(此时表达式不合法!),反之亦不合法)

只有非符号函数才可以参与运算(规约)操作,符号函数只能参与函数调用操作(要参与运算必须添加括号)。因此当函数作为参数传递时只能是前缀式的

1
2
f = / -- /不能参与运算
f = (/) -- ok
定义函数
1
2
3
4
a *-= b = a - b
(*-=) a b = a - b
f a b = a - b
a `f` b = a - b
调用函数
1
2
3
4
a *-= b
(*-=) a b
f a b
a `f` b
函数部分调用
  • 中缀方式,需要整体在外围添加括号。 中缀方式可以选择先传递第二个参数。
1
2
3
4
(/10)
(10/)
(`f` 10)
(2 `f`)

函数-不能部分调用第二个参数(-10),因为会被认为是负数,此时可以用subtract函数。subtract n返回一个把参数的值减去n的一个函数。subtract 10 100 == 90
通过中缀方式传递参数,可以指定传递第一个还是第二个参数:第一个(10/)和第二个(/10),任何函数都可以使用中缀方式调用,所以对于任意个数参数的函数总是可以跳过第一参数(好像没什么用?)

1
2
3
4
5
6
7
f x y z = (x,y,z)

fy = (`f` 'y') -- 跳过第一个参数,此时第三个参数变为第二个参数

fyz = (`fy` 'z') -- 继续跳过第一个参数

fyz 'x' -- ('x','y','z')
  • 前缀方式,直接按序给出参数即可
1
(/) 10
  • 函数部分调用后,参数类型约束会修改:
1
2
compare :: Ord a => a -> a -> Ordering
compare 1 :: (Ord a, Num a) => a -> Ordering
  • 通过部分调用可以简化函数定义(point free style
1
foo (<参数>*) = bar b \1  改为 foo = bar b
设置中缀函数的fixty和优先级

优先级设置了就不能再重新设置

由于值构造子也是函数,它也可以设置

1
(infixr|infixl) <priority> <符号函数名> -- 如果是非符号函数,那么需要加上反引号

模式

模式匹配:所有的赋值结构以及函数的定义(包括λ表达式)都能进行模式匹配

值的拆分:模式匹配后,可以进行值的拆分,拆分值:(或[(<name>,)*(或[ = (或[(<exp>,)*(或[<valueConstructor> <name>* = <exp>

调用函数时参数会进行模式匹配,会从上到下进行值的匹配:

1
2
3
f :: Int -> Int
f 0 = 1
f n = f (n-1) * n

还有拆分值的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

addVectors :: (Num a) => [a] -> [a] -> [a]
addVectors [x1, y1] [x2, y2] = [x1 + x2, y1 + y2]

-- 不合法
--addVectors ([x1, y1]++_) ([x2, y2]++_) = [x1 + x2, y1 + y2]
-- 括号必须,_也可匹配[]
addVectors (x1:y1:_) (x2:y2:_) = [x1 + x2, y1 + y2]

head' :: [a] -> a  
head' [] = error "Can't call head on an empty list, dummy!"  
head' (x:_) = x

拆分列表值

1
2
3
4
5
# t匹配一个长度为3,最后两个元素是2,3的列表
f :: [Int] -> [Int]; f (_:t@(_:2:3:[])) = 0:t; f _ = [];

f [2,1,2,3] == [0,1,2,3]
f [2,1,2,2] == []

列表生成式也有类似的功能

1
2
xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]  
[a+b | (a,b) <- xs]

guard

guard同样是从上到下判断的,只有表达式值为True才会调用。如果都为False那么继续找下一个模式。

otherwise = True

1
2
3
4
5
6
f :: Int -> Int
f 0 = 1
f n
    | n == 1 = 1
    | n <= 10 = f (n-1) * n
    | otherwise = error ("n to big: " ++ (show n))

where

where定义的量(可以定义名字和函数)只在本模式内可见,并且通过换行或分号分隔

如果有guard,where必须在本模式所有的guard后面

1
2
3
4
5
6
7
8
9
10
11
12
13
bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | bmi <= skinny = "You're underweight, you emo, you!"  
    | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | bmi <= fat    = "You're fat! Lose some weight, fatty!"  
    | otherwise     = "You're a whale, congratulations!"  
    where bmi = weight / height ^ 2  
          skinny = 18.5  
          normal = 25.0  
          fat = 30.0
    或者
    where bmi = weight / height ^ 2  
      (skinny, normal, fat) = (18.5, 25.0, 30.0)

let

有两种形式:

  • 第一种,它是一个表达式,任何能使用表达式的地方都能使用:
1
2
3
4
-- 每个部分必须存在
let <bindings> in <expression>
<bindings>: <binding>[(;|回车)<binding>]*
<binding>: <name> = <value>

let在<bindings>定义的量只在当前let表达式内可见

let表达式的值是<expression>的值

在函数中使用可以通过换行来分隔binding,此时;可写可不写

  • 第二种,没有in部分,用在do和列表生成式中,用来定义do和列表生成式中的局部的量:
1
let <bindings>

如果在ghci命令行中输入,不加in <expression>不是let表达式,相当于在外围定义量

case 表达式

1
case <exp> of (<pattern> -> <exp'>)+

<exp>的值从上到下进行<pattern>的匹配,匹配成功后返回<exp'>的值

1
2
3
4
5
6
7
8
head' :: [a] -> a  
head' [] = error "Can't call head on an empty list, dummy!"  
head' (x:_) = x

-- 可以用分号或者换行分隔
head' xs = case xs of 
    [] -> error "Can't call head on an empty list, dummy!"
    (x:_) -> x

lambda表达式

lambda也能进行模式匹配,但是因为它是匿名的,所以只能有一个模式

一般情况下,lambda 都是括在括号中,只要不会产生不平衡的括号,lambda的body会尽可能向右延伸。

1
2
addThree :: (Num a) => a -> a -> a -> a  
addThree = \x -> \y -> \z -> x + y + z
1
2
flip' :: (a -> b -> c) -> b -> a -> c  
flip' f = \x -> \y -> f y x

module

ghc中的模块源码.html文件在xxx\Haskell Platform\8.6.5\doc\libraries\base-4.12.0.0\src

The module name and the file name must be the same. Otherwise it can’t compile.

导入模块:

导入一个模块会自动导入所有的instance定义,但是 the instance is only available if the constructor is in scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<import>: [qualified] <module> [as] <prefix> [hiding] [<tuple>]
import { <import> }+

-- 导入全部
import Data.List

-- 选择性导入,只导入 nub和sort
import Data.List (nub,sort)

-- 除了 nub 其它全部导入
import Data.List hiding (nub)

-- 导入全部,引用时需要加上`M.`前缀
import qualified Data.Map as M

import qualified Data.Map == import qualified Data.Map as Data.Map

默认导入了Prelude模块,这个模块仅仅是导入一些其它模块的,然后导出一部分函数。

建立模块

1
2
3
4
5
6
7
8
-- 括号里是需要导出的函数、类型构造子、值构造子、还有typelcass中定义的内容
module <modlueName> [<exports>] where <code>
<exports> ::= (? (<functionName>|<type>|<typeclass>|<module>),* )? -- 没有:代表导出此模块中的所有内容,`()`代表都不导出
<fuctionName> ::= [^A-Z].*
<module> ::= module <moduleName'> [<exports>] -- 导出<moduleName>中导入的`<moduleName'>`模块的内容
<typeclass> :: = <typeclassName> ( ( <functiomName>* | .. ) ) -- `()`或者没有:代表不导出内部定义的内容
<type> ::= <typeConstructor>( (<valueConstructor>*|..) ) -- `()`或者没有:代表不导出type中的值构造子
<typeConstructor> | <valueConstructor> ::= [A-Z].*

I/O

I/O action(一个函数)代表一个会造成副作用的动作,常是指读取输入或输出到屏幕,同时也代表会回传某些值,类型是IO <type>

取出值:name <- IO <type>类型的值,name的类型将是<type>

可以使用do notation将许多I/O action打包,最后一个函数的返回值就是整体的返回值,可以使用return函数包装返回值(Monad m => a -> m aIO就是一个Monad,它只是个函数不会终止调用它的函数)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
main = do
    putStrLn "please enter an number:"
    run 33

run x = do
    number <- getLine
    if null number
    then return ()
    else do -- 必须用do打包成一个整体
        let num = read number :: Int
        if num == x
        then putStrLn "correct!"
        else do
            if num < x
            then putStrLn "less"
            else putStrLn "greater"
            run x

函数

  • putCharputStrputStrLnprint(putStrLn . show)
  • getChar(Enter 被按下的时候才会触发读取字符的行为,回车也会被获取到)
  • getLine

do

do notation 是一个语法糖,具体请看:Haskell类型.md 的 Monad 部分

1
2
3
4
5
6
7
8
9
10
11
main = do putStrLn "What is your name?"
          a <- getLine
          putStrLn "How old are you?"
          b <- getLine
          print (a,b)
# 解语法糖后:
main = putStrLn "What is your name?"
       >> getLine
       >>= \a -> putStrLn "How old are you?"
       >> getLine
       >>= \b -> print (a,b)