Haskell

文件扩展名为.hs
ghc {filename} 编译为可执行程序
runghc {filename} 直接运行
可执行的.hs文件必须有一个main函数
main = putStrLn "hello, world"
do block, 用于创建容纳多行语句的代码块
main = do
putStrLn "Hello, what's your name?"
name <- getLine
putStrLn $ "Hey " ++ name ++ ", you rock!"
getLine不是纯函数, 是有副作用的I/O操作,
这意味着返回值类型为IO String, 不能直接使用(被设计用于区分纯函数和非纯函数),
需要用专用的读入运算符 <- 将I/O操作的返回值绑定给name, name的类型为 String -> String
I/O操作只在被绑定给main或用于do表示法时才会被执行.
return函数: return ()
Haskell的return函数和一般编程语言都不同, return并不会"返回"什么,
即使运行多次return, 也不会结束代码执行.
return函数的作用是将一个纯值包装成非纯的I/O操作.
return函数的主要作用是在main函数的条件语句中创造一条什么都不做的路径(Haskell的条件语句不能省略else),
就像Python的 pass, Rust的 ().
catch函数: main = toTry `catch` handler
Haskell只有在I/O操作能够处理异常, 即使异常并非因I/O操作导致(比如除零溢出).
import System.Environment
import System.IO
import System.IO.Error
main = toTry `catch` handler
toTry :: IO ()
toTry = do
(fileName:_) <- getArgs
contents <- readFile fileName
putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines"
handler :: IOError -> IO ()
handler e
| isDoesNotExistError e = putStrLn "The file doesn't exist!"
| otherwise = ioError e
undefined在Haskell里指错误的值, 将该值打印出来会引发异常.
:l baby 载入baby.hs文件
:m {模块名} 载入模块, 用空格分隔多个模块名
:t {变量名} 查看变量的类型
:t (==) 查看运算符类型
(==) :: (Eq a) => a -> a -> Bool
:: 后为类型注释
=> 前为类型约束(typeclass, 类似于Rust的trait)
read "5" :: Int 带有类型注释的函数调用(用以明确将"5"转换为哪种类型)
:info {type or typeclass} 查询类型和instance
定义行为的接口, 跟Rust的trait是相近的概念
  • Eq 可判断相等性
  • Ord 可比较大小
  • Show 可表示为字符串
  • Read 可从字符串转化为值, 与Show相反
  • Enum 可枚举, 有前置子(predecesor)和后置子(successer)
  • Bounded 有上限(maxBound)和下限(minBound)
  • Num 实数和整数
  • Integral 整数
  • Floating 浮点数
do实际上是 >>= 的语法糖, 可见Monad在函数式语言里还起到了模拟"代码行"的作用.
在do表达式里, 所有不带 let 的行都是Monad值, 要获取结果只能使用 <-.
foo :: Maybe String
foo = Just 3 >>= (\x ->
Just "!" >>= (\y ->
Just (show x ++ y)))
等于
foo :: Maybe String
foo = do
x <- Just 3
y <- Just "!"
Just (show x ++ y)
Just 9 >>= (\x -> Just (x > 8))
等于
marySue :: Maybe Bool
marySue = do
x <- Just 9
Just (x > 8)
实际上也可以将do表达式的最后一行替换为return函数调用,
比如 return (x > 8), 这样代码会更接近命令式编程语言.
当do表达式里的模式匹配失败时(例如从列表取元素), 会调用fail函数.
fail函数的默认实现会调用error函数, 引发程序崩溃.
因此, Maybe将fail函数重写为 fail _ = Nothing, 忽略错误并返回Nothing.
Functor 函子/可映射类型, 需要实现该类型用于映射的fmap函数.
fmap函数有两种理解方式, 这两种理解都是正确的:
  1. 1.
    接受函数和函子值(即容器内部的值), 在函子值上映射这个函数
  2. 2.
    接受函数, 把它提升为可以操作函子值的函数
    设计出这个类型, 是为了让普通的映射函数能够操作各种实现了fmap"接口"的具有上下文的类型的值, 让映射函数通用化.
Haskell的Functor类型类仅仅是函子在语法层面的接口, 并不能约束人们对fmap函数的实现.
为了让函子能够符合其性质, 程序员在创建实现函子的类型时, 需要自觉遵守以下的函子定律:
  1. 1.
    调用fmap之后, 返回的类型与原来的值相同(显而易见, 映射一个列表返回的结果也是列表)
  2. 2.
    结合律: fmap (f . g) = fmap f . fmap g (fmap的函数组合和连续调用函数的结果一致)
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
简称为ap, 该类型与函子的区别在于, 容器内部的值可以是一个函数:
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
即applicative functor可以将一个值包装为函子类型, 同时, 该值还可以是一个函数.
当值为函数的函子 f (a -> b) 和值为非函数的函子 f a 调用 <*> 运算符时,
将把值为函数的函子作为映射函数使用, 进而制造出映射函数的返回值的函子类型, 也就是 f b.
设计出这个类型, 是为了让函子类型能够实现和"参数为函子类型的部分调用函数"一起工作:
把通用的部分调用函数上升(lift)到可以操作特定容器里的值, 免得频繁使用fmap.
在Haskell以外的语言里, ap会作为这个上升后的部分调用函数的方法, 其参数是一个特定容器.
由于结合律的存在, 我们可以互换容器与函数的位置.
举例来说, 以数值类型为例, 可以做到如下部分调用:
ghci> (+3) 9
12
但对函子类型来说, 这是不可能的:
ghci> (Just (+3)) (Just 9)
error
ghci> (+(Just 3)) (Just 9)
error
引入applicative functor以后, 可以用中缀运算符 <*> 实现我们需要的操作:
ghci> (Just (+3)) <*> (Just 9)
Just 12
ghci> (Just (+3) <*>) Just 9
Just 12
ghci> Just (+3) <*> Just 9
Just 12
我们也可以直接用pure函数包装运算符:
ghci> pure (+) <*> Just 3 <*> Just 9
Just 12
此处的 pure (+) <*> Just 3 创造出 Just (+3), 后续跟原来的版本一样.
为了提高可读性, 增加了 <$> 中缀运算符,
(+) <$> Just 3 等于原来的 pure (+) <*> Just 3, 从此代码看起来更接近普通运算符的调用:
ghci> (+) <$> Just 3 <*> Just 9
Just 12
Monad指支持绑定(bind)运算符 >>= 的Applicative Functor:
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
绑定运算符等同于如下的applyMaybe函数, 允许一个带有上下文的值(Just 3)应用一个函数,
这个函数会从获取该值(3), 返回一个具有相同上下文的新值(Just 4).
applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
applyMaybe Nothing f = Nothing
applyMaybe (Just x) f = f x
ghci> Just 3 `applyMaybe` \x -> Just (x+1)
Just 4
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
fail :: String -> m a
fail msg = error msg
Monad虽然没有Applicative Functor的类型约束, 但它仍然是一种Applicative Functor,
没有类型约束是因为Monad先于Applicative Functor出现, 这是一个历史问题.
return函数和Applicative Functor的pure是相同的, 可以将值包装成满足Monad的类型.
>>= 绑定运算符让程序员能够在不解除类型包装的情况下对值进行操作, 这非常像链式操作,
只不过链式操作的方法都是比较具体的,
而绑定运算符允许塞入任何高阶函数(因为代数数据类型可以用模式匹配解除上下文取得实际的值).
>> 运算符用于准备计算结果(x)的默认值或失败值(y), 其作用相当于大多数语言的 ?,
用来避免编写冗长的条件判断代码(在Haskell里指类似Maybe类型的模式匹配, 避免写一大堆的Nothing匹配).
JavaScript的 Promise.prototype.then 就是Promise这个Monad的 >>= 运算符,
只不过也允许Promise以外的返回值罢了.
在Array.prototype里也有很多函数看起来就类似于Monad的 >>= 运算符与特定处理函数的合体.
把绑定运算符写成TypeScript函数如下:
function applyMonad<T, U extends Monad<T>>(this: U, (val: T) => U): U {
...
}
可见主要区别只是JavaScript没有内置的代数数据类型(但有fantasy-land这样的ADT项目), 只能用类包装值而已.
Monad需要自觉遵守的定律:
  1. 1.
    左单位元 return x >>= f 等于 f x
  2. 2.
    右单位元 m >>= return 等于 m
  3. 3.
    结合律 (m >>= f) >>= g 等于 m >>= (\x -> f x >>= g)
    这条定律似乎并没有提供什么真正的"结合性", 两条表达式的实际运行步骤是一样的,
    都是先调用 f x, 再调用 g x.
Haskell的列表推导式也是Monad的语法糖.
Mononid接口代表着该值能够被合并/联接, 哪怕值是函数.
Monoid 由一个满足结合律的二元函数和一个单位元组成.
单位元指与函数中其他任何参数做运算时, 总是等于那个参数的值.
对于乘法运算符 * 来说, 单位元就是1.
对列表联结运算符 ++ 来说, 单位元就是 [].
class Monoid m where
mempty :: m
mappend :: m -> m -> m
mconcat :: [m] -> m
mconcat = foldr mappend mempty
mempty表示Monoid的单位元, 这个函数实际上相当于常量.
mappend的名字起得不好, 它实际上指的是参数和结果都为相同类型的二元函数, 中缀运算符一般都满足这一点.
mconcat表示由m值组成的列表能够被折叠成一个m值, 其默认实现里使用了Monoid的单位元.
和函子一样, Monoid需要程序员自觉符合如下定律:
  1. 1.
    mempty `mappend` x = x 单位元与值 x 通过二元函数调用后, 结果是值 x, 也就是单位元的含义
  2. 2.
    x `mappend` mempty = x 与上一条相同, 只是位置互换
  3. 3.
    (x `mappend` y) `mappend` z = x `mappend` (y `mappend` z) 结合律
类似于Rust的trait, 只不过Haskell是用模式匹配实现
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x ==y)
创建一个基于另一个typeclass的typeclass
类型约束 => 类型, 下例是为 Num a 添加 Eq a 的类型约束:
class (Eq a) => Num a where
...
手工为类型实现typeclass(不使用deriving语法), 类似于Rust的impl, 只不过Haskell是用模式匹配实现:
instance Eq TrafficLight where
Red == Red = True
Green == Green = True
Yellow == Yeow = True
_ == _ = False
instance Show TrafficLight where
show Red = "Red light"
show Yellow = "Yellow light"
show Green = "Green light"
kind是一个用于描述类型的类型的概念
拿Maybe举例, Maybe不是一个具体类型(需要结合其他类型进行使用, 比如Just和Nothing), 它的kind是:
ghci> :k Maybe
Maybe :: * -> *
有两个星号, 表示Maybe类型需要先得到一个类型, 然后才能接触到值.
Int是一个具体类型(直接拥有值), 它的kind是:
ghci> :k Int
Int :: *
只有一个星号, 表示Int类型的下一级直接就是具体的值.
Maybe Int是Maybe和Int的组合, 表示一个已经得到Int类型的Maybe类型, 它的kind是:
ghci> :k Maybe Int
Maybe Int :: *
只有一个星号, 表示Maybe Int的下一级直接就是具体的值. 可见, Int类型填充了 Maybe :: * -> * 的第一个星号.
  • Bool 布尔类型, 包含 TrueFalse
  • Int 有符号整数, 在 Data.Int 模块里另有 Int8, Int16, Int32, Int64
  • Word 无符号整数, 在Data.Word模块里另有 Word8, Word16, Word32, Word64
  • Integer 任意精度整数(用于大数计算)
  • Float 单精度浮点数
  • Double 双精度浮点数
  • Rational 分数, 例 4.1332::Rational (输出 10333 % 2500, %前为分子, %后为分母)
  • Char 字符
  • String 字符串, 为 [Char] 的别名
  • (a, b) 元组(Tuple), Haskell的元组最多支持62个元素
  • () 空元组, 或者unit类型, 作为只有副作用, 没有返回值的函数的返回值
  • [1, 2, 3, 4, 5] 列表(List), 要求元素类型相同
    [1..5] 生成列表(支持浮点数, 但会有编程中常见的精确度问题, 不建议使用浮点数):
    [1, 2, 3, 4, 5]
[1..] 生成列表(无限, 用于惰性计算)
[1, 2, 3, 4................
[1, 4..20] 根据步长生成列表(第2个元素值减去第1个元素值即为步长):
[1, 4, 7, 10, 13, 16, 19]
[x * 2 | x <- [1..10], x * 2 >= 12] 列表推导式(有严格的书写顺序要求):
[12, 14, 16, 18, 20]
推导式的第一部分是输出值(表达式, 可在此使用if else, 也可以嵌套列表推导式),
第二部分是取值范围(用逗号分隔能编写多个变量的取值范围),
第三部分是限制条件(也叫谓词predicate, 可选, 用逗号分隔能编写多个限制条件).
Haskell里函数调用具有最高优先级, 运算符也是函数, 但运算符有各自的优先级数值.
- 存在两种 - 运算符, 一种是前缀函数, 一种是中缀函数, 前者是负数, 后者是减法运算,
表示负数时, 必须以括号形式 (-5) 调用.
&& 逻辑与
|| 逻辑或
not 逻辑非
== 相等
/= 不等
++ 列表合并
: 将左值插入列表(右值)前端
!! 根据索引(右值)对列表(左值)取值, 索引从0开始
. 函数组合(compose), 要求左值和右值的函数都只有一个参数, 如果存在多个参数, 则应该部分调用至只剩一个.
函数组合常用来简化括号
replicate 2 (product (map (*3) (zipWith max [1,2] [4,5])))
replicate 2 . product . map (*3) $ zipWith max [1,2] [4,5]
固定性声明(fixity declaration)用于将函数定义为运算符,
被定义为运算符的函数在中缀函数形式调用时无需用 ` 包裹函数名.
右结合运算符: infixr 优先级 函数名
左结合运算符: infixl 优先级 函数名
infixl 7 * 表示函数 (*) 的优先级为7, 左结合
infixl 6 + 表示函数 (+) 的优先级为6, 左结合
格式: if 条件 then 为真时的语句 else 为假时的语句
else 不可省略.
doubleSmallNumber x = if x > 100
then x
else x * 2
在Haskell, 函数是一等公民.
Haskell的函数定义位置是任意的, 不需要定义在使用函数之前.
最常见的函数调用是前缀函数(prefix function)形式, 但Haskell也允许我们以中缀函数(infix function)形式调用函数.
函数的本质是一种模式匹配映射(从参数到结果), 代码会自上而下是否有可匹配的模式, 匹配到模式后就会返回.
当函数不具备可匹配的模式时, 就会抛出错误.
函数的第一行为类型签名, 类型签名里的 a, b, c 等字符是一种约定俗成的通配符, 被称作"类型变量",
类似于泛型, 这种通配符让Haskell实现了函数的多态性.
Haskell函数都是柯里化函数.
名称后不带 ' 的函数都是惰性函数(惰性函数有时会遇到栈不够用的问题, 需要用带有 ' 的严格计算版本).
以下创建一个函数lucky, 在模式匹配到数字7时, 会返回不同的结果
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
Haskell没有循环语句, 使用递归解决一切. 编写递归的重点是每个边界条件只需考虑自身处境.
阶乘函数
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
当一个函数是二元函数时(只有两个参数), 可以用中缀函数的形式调用.
92 `div` 10 等同于 div 92 10
Haskell函数只支持一次一个参数的调用, 所有的"多参数函数"其实都是柯里化的函数.
留白位置决定了参数位置, 必须用括号包裹
divideByTen = (/ 10)
TenDivideBy = (10 /)
isUpperAlphanum = (`elem` ['A'..'Z'])
出于代码可读性的考虑, 有时需要在源码完成对各个模式进行文本对齐.
_ 可以忽略掉匹配值
middle :: (a, b, c) -> b
middle (_, x, _) = (1, 2, 3)
(x:y:z) 的方式可以匹配到任意超过2个元素的列表,
x是第一个元素, y是第二个元素, z是剩余元素, 当元素数量为2时, z的值为空列表 []
匹配列表头(第一个元素)
head' :: [a] -> a
head' [] = error "Can't call head on an emtpy list, dummy!"
head' (x:_) = x
匹配列表尾(第一个元素以外的元素)
tail' :: [a] -> a
tail' [] = error "Can't call tail on an emtpy list, dummy!"
tail' (_:x) = x
as模式在模式匹配同时保留整体引用
语法为 变量名@匹配模式
capital :: String -> String
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
一种类似多重if语句的模式, otherwise匹配剩余的一切可能性
以下是一个将BMI转换为单词的函数, 注意 bmiTell bmibmi 才是该函数参数的模式匹配,
后续 | 的内容是匹配成功后进一步的条件匹配.
bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "underweight"
| bmi <= 25.0 = "normal weight"
| bmi <= 30.0 = "overweight"
| otherwise = "obesity"
guard也可以写成一行(可读性差, 不推荐)
max' :: (Ord a) => a -> a -> a
max' a b | a > b = a | otherwiese = b
case表达式是模式匹配脱糖后的写法. 模式匹配只能用于函数定义, 而case表达式可以用在任何地方.
head' :: [a] -> a
head' [] = error "No head for empty lists!"
head' (x:_) = x
等价于
head' xs = case xs of [] -> error "No head for empty lists!"
(x:_) -> x
where绑定在当前函数的底部定义变量, 变量对同一个函数的其他模式不可见.
主要用来为当前模式提供中间数据(注意, where的缩进是必须的)
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= skinny = "underweight"
| bmi <= normal = "normal weight"
| bmi <= fat = "overweight"
| otherwise = "obesity"
where
bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
可以在where生成辅助函数
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
where
bmi weight height = weight / height ^ 2
let表达式定义局部变量.
格式: let <bindings> in <expressions>
bindings 运行赋值表达式, 可以赋值多个表达式, 如果写在一行则需要用 ; 分隔它们
expressions 局部变量可见的作用域, 在此使用 bindings 里赋值的变量, 表达式的结果作为返回值
in <expressions> 在ghci里可以省略
add :: (Num a) => a -> a -> a
add a b = let left = a; right = b in left + right
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]
可以以中缀形式进行模式匹配(与函数调用无关, 任何函数都可以中缀模式调用)
myCompare :: (Ord a) => a -> a -> Ordering
a `myCompare` b
| a > b = GT
| a == b = EQ
| otherwise = LT
高阶函数签名中的函数参数需要放在括号里, 以表明这是一个函数.
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
将运算子作为参数时, 只需要加上括号, 例如 (+) 就是 + 的函数形式.
\ 开头的表达式, 用于创建匿名函数, 也可以嵌套lambda表达式
\x -> x > 15
最常见的函数调用方式, 优先级最高, 左结合.
sum map sqrt [1..130]
实际上的函数调用顺序:
((sum map) sqrt) [1..130]
加上括号改变调用顺序:
sum (map sqrt [1..130])
这是空格以外的另一种函数调用方式, 优先级最低, 右结合.
位于 $ 右侧的部分会先被计算.
sum $ map sqrt [1..130]
实际上的函数调用顺序:
sum ((map sqrt) [1..130])
省去了不必要的括号输入
$ 用于柯里化
map ($ 3) [(4+), (10*), (^2), sqrt]
import 模块名 载入模块
import 模块名 (成员1, 成员2) 仅载入模块中的部分成员
import 模块名 hiding (成员1, 成员2) 排除模块中的部分成员
import qualified 模块名 限定导入, 以带有命名空间的形式载入模块
import qualified 模块名 as 命名空间 以别名的形式载入模块
文件名与模块名一致.
导出类型时, 如果不包含值构造器, 那么导入该模块的用户将无法使用值构造器
(比如模式匹配, 比如直接从值构造器创造新值), 有时这能提供更高的抽象(不暴露内部实现).
用类型 (..) 可以导出类型的全部值构造器.
拿Shape举例, Shape(..) 等价于 Shape(Circle, Rectangle)
module Geometry
( Shape(..)
, sphereVolume
, sphereArea
) where
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)
sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)
通过文件夹作为命名空间, 对模块进行分层的结构
Geometry目录
Geometry/sphere.hs 对应 module Geometry.Sphere
Geometry/cuboid.hs 对应 module Geometry.Cuboid
相当于Rust的enum, 用于创建全新的类型.
data 类型 = 值构造器
data Bool = False | True
data 类型 = 值构造器 deriving (typeclass1, typeclass2)
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
此处真正声明的类型只有Shape一个, Circle和Rectangle并不是类型
值构造器的本质是一个返回某种数据类型值的函数
:t Circle
Circle :: Float -> Float -> Float -> Shape
:t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape
值构造器可以像函数一样部分应用
map (Circle 10 20) [4,5,6,6]
用于创建类型别名.
type IntList = [Int]
用于将已有的类型包裹为新类型.
newtype ZipList a = ZipList { getZipList :: [a] }
newtype和data关键字的功能相同, 都是用来创造新的类型,
区别在于newtype只能有一个值构造器, 且该值构造器只能有一个字段.
此外, newtype没有自动装箱和解除装箱的性能消耗, 如此看来, newtype更像是一种类型别名.
data Person = Person {
firstName :: String
, lastName :: String
} deriving (Show)
记录语法会自动生成用于访问对应成员的函数
:t firstName
firstName :: Person -> String
Person { firstName="Jonathan", lastName="Joestar" }
data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)
Cons (constructo, 构建)指的是Lisp中的二元元组 (value, List), 也称为"序对",
展开为 (value, (value, (value, (value, null), 功能基本上等同于函数式的链表.
a (List a) 就是 (a, List), 是 Cons
: 是右结合运算符, 所以
[5] 实际上是 5:[] 的语法糖
[4, 5]4:(5:[]) 的语法糖