如何在 OI 中愉快地 Haskell
HoshinoTented
2019-07-27 10:51:53
**这篇文章可能需要之前没有介绍过的前置知识,在本文中会粗略介绍**
纯函数式的 Haskell 在 OI 中时常会遇到一些问题
这篇文章将带你愉快地在 OI 中使用 Haskell
~~当然是仅限练习,考场上可是没有 Haskell 的~~
# 基础操作
一些基础内容可以看 [洛谷日报的 #188 期](https://www.luogu.org/blog/i-love-illya/haskell-ru-men-yu-fa-shou-ce)
## 数值运算
首先讲解对数值的操作
比如 `+-*/`
首先是 `(+) (-) (*)`
```haskell
> :i Num
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
-- ...
```
它们都被定义在了 `Num` 类型类里
```haskell
> 1 + 2
3
> 1 - 2
-1
> 1 * 2
2
```
接下来是 `(/)`
但是如果我们看一下 `(/)` 的定义。。。
```haskell
class Num a => Fractional a where
(/) :: a -> a -> a
...
-- Defined in ‘GHC.Real’
infixl 7 /
```
需要 `Fractional` 类型类,我们再看看 `Fractional` 类型类的定义
```haskell
> :i Fractional
class Num a => Fractional a where
(/) :: a -> a -> a
-- ...
instance Fractional Float -- Defined in ‘GHC.Float’
instance Fractional Double -- Defined in ‘GHC.Float’
```
只有 `Float` 和 `Double` 实现了 `Fractional` 类型类
这就意味着,我们不能对 `Int` 使用 `(/)` 函数了!
但我们可以使用 `div`
```haskell
> :i div
class (Real a, Enum a) => Integral a where
div :: a -> a -> a
infixl 7 `div`
> :i Integral
class (Real a, Enum a) => Integral a where
div :: a -> a -> a
instance Integral Int -- Defined in ‘GHC.Real’
```
于是可以这样进行除操作
```haskell
> div 5 2
2
```
或者将 `div` 函数放在中间
```haskell
> 5 `div` 2
2
```
最后是取模
```haskell
> :t mod
mod :: Integral a => a -> a -> a
> 3 `mod` 2
1
```
Haskell 并没有提供 `(%)` 运算符,不过你可以自己定义
```haskell
> (%) = mod
> 3 % 2
1
```
## 位运算
位运算也是 OI 中很重要的知识
Haskell 的位运算函数都包含在了 Data.Bits 包中
```haskell
import Data.Bits
```
使用 `(.&.) (.|.)` 和 `xor` 来进行 且 或 异或 操作
```haskell
> 2 .&. 1
0
> 2 .|. 1
3
> 2 `xor` 1
3
```
使用 `shiftL` 和 `shiftR` 来进行位移
```haskell
> 1 `shiftL` 9
512
> 512 `shiftR` 9
1
```
你甚至可以定义一个 `(<<)` 运算符
```haskell
> (<<) = shiftL
> 1 << 9
512
```
不过要注意的是,`(>>)` 被 Monad 使用了,定义 `(>>)` 运算符之前记得先隐藏 Prelude 中的 `(>>)` 运算符
# 链表
Haskell 内置的列表就是一个链表实现
```haskell
> :i []
data [] a = [] | a : [a]
```
## 构造列表
可以通过空列表的构造器 `[]` 和 连接列表的构造器 `(:)` 来构造一个列表
```haskell
> 1:2:3:4:5:[]
[1,2,3,4,5]
```
当然也可以直接 `[1, 2, 3, 4, 5]`
```haskell
> [1, 2, 3, 4, 5]
[1,2,3,4,5]
```
通过 `[begin..end]` 来构造一个区间
```haskell
> [1..10]
[1,2,3,4,5,6,7,8,9,10]
```
构造一个等差序列
```haskell
> [1, 2..10]
[1,2,3,4,5,6,7,8,9,10]
> [1, 3..10]
[1,3,5,7,9]
```
使用列表生成器
```haskell
> [x | x <- [1..5], even x]
[2,4]
> [y | y <- [1..5], even y]
[2,4]
> [x + y | x <- [1..5], y <- [1..5], even x && even y]
[4,6,6,8]
```
## 列表常用函数
这里将介绍部分列表常用函数(同时也是对 [洛谷日报的 #188 期](https://www.luogu.org/blog/i-love-illya/haskell-ru-men-yu-fa-shou-ce) 的补充)
名称(及函数签名) | 模块 | 作用
--------------|------|-------
head :: [a] -> a | Prelude | 取出列表的首元素
last :: [a] -> a | Prelude | 取出列表的尾元素
init :: [a] -> [a] | Prelude | 取出除了最后一个元素以外的元素
tail :: [a] -> [a] | Prelude | 取出除了第一个元素以外的元素
take :: Int -> [a] -> [a] | Prelude | 获取列表前 n 个元素
drop :: Int -> [a] -> [a] | Prelude | 删除列表前 n 个元素
(++) :: [a] -> [a] -> [a] | Prelude | 连接两个列表
(!!) :: [a] -> Int -> a | Prelude | 随机访问
elem :: (Foldable t, Eq a) => a -> t a -> Bool | Prelude | 检查目标元素是否存在于列表中
length :: Foldable t => t a -> Int | Prelude | 返回列表长度
map :: (a -> b) -> [a] -> [b] | Prelude | 对列表的每个元素应用函数,并返回函数结果的列表
filter :: (a -> Bool) -> [a] -> [a] | Prelude | 过滤列表中元素
reverse :: [a] -> [a] | Prelude | 翻转列表
sort :: Ord a => [a] -> [a] | Data.List | 对列表排序(从小到大)
sortBy :: (a -> a -> Ordering) -> [a] -> [a] | Data.List | 根据传入的比较函数对列表排序
# IO Monad
Haskell 是纯函数式语言,但 OI 中经常涉及读入输出操作,这些都是不纯的,应该怎么办呢
Haskell 给出了解决方案:`IO Monad`
## 浅谈 Monad
要理解 `IO Monad`,首先就要知道什么是 `Monad`
那什么是 `Monad` 呢?
让我们看看 `Monad` 的定义:
```haskell
> :i Monad
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
{-# MINIMAL (>>=) #-}
-- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
```
最重要的是 `(>>=)` 和 `return`
`return` 用于接收一个值,并把这个值包装进 `Monad` 里
`(>>=)` 用于取出值,并把值应用到传入的函数,然后返回一个新的 Monad
但无论是 `return` 还是 `(>>=)`
它们都不能把值从 `Monad` 中取出并返回
这也就意味着,如果一个值进入了 `Monad`,就再也无法通过 `Monad` 提供的操作出来了。。。
而 `Monad` 的这个特性,恰好也是 `IO Monad` 的本质
一旦函数中使用了 `IO Monad`,那么这个函数的返回值也一定会带有 `IO Monad`
所以也就能区分纯函数和不纯函数了
## 何为 (>>=)
`(>>=)` 读作 bind
bind 是 Monad 中最重要的操作,Monad 定义中的 `{-# MINIMAL (>>=) #-}` 代表了实现 Monad 至少要实现 `(>>=)` 这个函数
我们首先看看 `(>>=)` 的函数签名
```haskell
> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
```
它意为 接收一个 `Monad`,进行一次运算,然后再返回一个 `Monad`
但要如何使用 `(>>=)` 呢
让我们先看看 `putStrLn` (用于输出字符串,下面会提到)
```haskell
> :i putStrLn
putStrLn :: String -> IO ()
```
它接收一个 `String`,然后返回一个 `IO ()`
不过 `IO` 这个类型实现了 `Monad`
我们这样调用 `putStrLn`
```haskell
> putStrLn "qwq"
qwq
```
但我们想要在一条语句里调用两次 `putStrLn`
要怎么做呢?
答案是: 使用 `(>>=)`
首先,我们先分别调用两次 `putStrLn`
```haskell
> putStrLn "one"
one
> putStrLn "two"
two
```
然后使用 `(>>=)` 把这两个操作连接起来
```haskell
> putStrLn "one" >>= \_ -> putStrLn "two"
one
two
```
或者直接使用 `(>>)`
```haskell
> putStrLn "one" >> putStrLn "two"
one
two
```
还可以调用更多次的 `putStrLn`
```haskell
> :{
| printN :: Int -> String -> IO ()
| printN 0 _ = return ()
| printN n str = putStrLn str >> printN (n - 1) str
| :}
> printN 5 "qwq"
qwq
qwq
qwq
qwq
qwq
```
Monad 的 `(>>=)` 可以用来进行连续的计算
也使得 纯函数式的 Haskell 可以以命令式的方式来编写程序
Haskell 还为 `(>>=)` 提供了一个语法糖: `do`
比如:
```haskell
main :: IO ()
main = getLine >>= \s ->
putStrLn s
```
等价于
```haskell
main :: IO ()
main = do
s <- getLine
putStrLn s
```
这样就显得更加命令式了
`IO Monad` 的部分极为重要,无法掌握这部分知识代表着无法让 Haskell 程序与系统交互,从而也无法解决题目
## 输出
你可以使用这两个函数来输出: `putStrLn` 和 `print`
```haskell
> putStrLn "qwq"
qwq
> print "qwq"
"qwq"
```
会发现两者的输出不太一样,但如果我们看看 [print 的实现](https://hackage.haskell.org/package/base-4.12.0.0/docs/src/System.IO.html#print)
会发现它其实是:
```haskell
print x = putStrLn (show x)
```
或者
```haskell
print = putStrLn . show
```
这两者是等价的
首先通过 `show` 函数把值转化为 `String`,再使用 `putStrLn` 输出
## 输入
可以使用 `getChar` 或者 `getLine` 来读入一个字符或者一整行
```haskell
> getChar
1
'1'
> getLine
qwq
"qwq"
```
使用 `words` 来根据空格分割
```haskell
> words <$> getLine
1 2 3
["1","2","3"]
```
使用 `read` 将字符串转化为 `Int`
```haskell
> map read . words <$> getLine :: IO [Int]
1 2 3
[1, 2, 3]
```
## Text
Haskell 自带的 `String` 效率低下,这使得我们需要使用更高效的 `String`,即 Data.Text
```haskell
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
```
`Data.Text` 是 text 本体
`Data.Text.IO` 提供了关于 `Text` 的 IO 操作
比如
```haskell
> map (read . T.unpack) . T.words <$> TIO.getLine :: IO [Int]
1 2 3
[1, 2, 3]
```
## 读入优化
使用 `read` 来转换数据实在是太慢了,这使得我们需要自己写一个转换器
```haskell
-- Parse Int :: Count -> Source -> Result
parseInt :: Int -> String -> Int
parseInt n [] = n
parseInt n (char:str) = parseInt (n * 10 + (ord char - ord '0')) str
main :: IO ()
main = do
xs <- map (parseInt 0 . T.unpack) . T.words <$> TIO.getLine :: IO [Int]
print xs
```
这样,我们就完成了 Haskell 中的读入优化
# 数组
列表是链表实现,OI 中常常需要随机访问,所以列表并不胜任这份工作
虽然 Haskell 有内置的 Data.Array,但性能和易用性方面还是太差了
这里介绍 [vector 库](http://hackage.haskell.org/package/vector)
## 不可变 Vector
首先导入 Data.Vector.Unboxed (Data.Vector 也是可以的,但 Unboxed 版本的 Vector 对实现了 Unbox 类型类的类型 (例如 Int) 有空间和时间优化,适合 OI 使用)
```haskell
import qualified Data.Vector.Unboxed as V
```
什么是 `qualified`?
`qualified` 关键字代表这个包不直接引入到全局命名空间里
而 `as` 则是将包重命名
比如这里重命名为 `V`
### 构造 Vector
首先,你可以创建一个空 `Vector`
```haskell
> :i V.empty
V.empty :: V.Unbox a => V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
```
或者创建只有一个元素的 `Vector`
```haskell
> :i V.singleton
V.singleton :: V.Unbox a => a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
```
时间复杂度都是 $O(1)$
创建一个带有 n 个 i 的 `Vector`
```haskell
> :i V.replicate
V.replicate :: V.Unbox a => Int -> a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
```
可以像这样使用
```haskell
> V.replicate 5 1 :: V.Vector Int
[1,1,1,1,1]
```
或者创建带有 n 个元素的 `Vector`
```haskell
> :i V.generate
V.generate :: V.Unbox a => Int -> (Int -> a) -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
```
`generate` 接收一个 `Vector` 长度,和一个 `(Int -> a)` 的函数,意思是传入一个数组下标,返回一个数组值
最后返回一个生成好的 `Vector`
或者你可以选择递推的方式构造出一个 `Vector`
```haskell
> :i V.iterateN
V.iterateN :: V.Unbox a => Int -> (a -> a) -> a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
```
`iterateN` 接收一个 `Vector` 长度, 一个 `(a -> a)` 的函数,代表接收上一个值,返回当前值
然后再接收一个初始值,最后返回构造好的 `Vector`
比如我们使用 `iterateN` 构造一个斐波那契数列
```haskell
> (V.iterateN 10 (\(a, b) -> (b, a + b)) (0, 1)) :: V.Vector (Int, Int)
[(0,1),(1,1),(1,2),(2,3),(3,5),(5,8),(8,13),(13,21),(21,34),(34,55)]
```
发现元组的第一项是 0, 1, 1, 2, 3, 5, 8, 13 ...
这的确是斐波那契数列
你还可以把一个 Haskell 列表转化为 `Vector`
```haskell
> :t V.fromList
V.fromList :: V.Unbox a => [a] -> V.Vector a
> V.fromList [1, 1, 4, 5, 1, 4] :: V.Vector Int
[1,1,4,5,1,4]
```
这些函数的时间复杂度是 $O(n)$
### 访问 Vector
我们导入 Data.Vector.Unboxed 中的访问运算符,可以简化我们的代码
```haskell
import Data.Vector.Unboxed ((!))
```
然后就可以使用这种方式来随机访问 `Vector` 了
```haskell
> :i (!)
(!) :: V.Unbox a => V.Vector a -> Int -> a
-- Defined in ‘Data.Vector.Unboxed’
> v = V.generate 5 id
> v ! 3
3
```
时间复杂度是 $O(1)$
### 连接 Vector
解决问题的时候,避免不了往 `Vector` 中添加值
使用 `cons` 把 值 添加在 `Vector` 的前端
```haskell
> :i V.cons
V.cons :: V.Unbox a => a -> V.Vector a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
> V.cons 1 (V.fromList [2, 3, 4]) :: V.Vector Int
[1,2,3,4]
```
或者使用 `snoc` (这不就是把 `cons` 反过来写了吗!) 把 值 添加在 `Vector` 后端
```haskell
Prelude V Data.Vector.Unboxed> :i V.snoc
V.snoc :: V.Unbox a => V.Vector a -> a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
> V.snoc (V.fromList [1, 2, 3]) 4 :: V.Vector Int
[1,2,3,4]
```
时间复杂度都是 $O(n)$
使用 `(++)` 来连接两个 `Vector`
不过使用 `(++)` 之前还需要隐藏 Haskell 自带的 `(++)`
```haskell
import Prelude hiding ((++))
```
然后导入 `Vector` 的 `(++)`
```haskell
import Data.Vector.Unboxed ((++))
```
然后连接两个 `Vector`
```haskell
> :i (++)
(++) :: V.Unbox a => V.Vector a -> V.Vector a -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
infixr 5 ++
> V.fromList [1, 2, 3] ++ V.fromList [4, 5, 6] :: V.Vector Int
[1,2,3,4,5,6]
```
时间复杂度是 $O(n + m)$
### 修改 Vector
不能被修改的 `Vector` 不是好 `Vector`!
可以通过 `(//)` 运算符来实现修改值
```haskell
> import Data.Vector.Unboxed ((//))
> :i (//)
(//) :: V.Unbox a => V.Vector a -> [(Int, a)] -> V.Vector a
-- Defined in ‘Data.Vector.Unboxed’
```
接收一个被修改的 `Vector`,和一个 `(下标, 新值)` 的列表,最后返回一个更新完的 `Vector`
不过要注意的是,原本的 `Vector` 并不会被修改
所以复杂度是 $O(m + n)$,$m$ 是 `Vector` 的长度,$n$ 是更新列表的长度
```haskell
> V.fromList [1, 2, 3, 4] // [(1, 5)] :: V.Vector Int
[1,5,3,4]
```
## 可变 Vector
不可变 `Vector` 有很多缺点,比如在 oi 中十分致命的性能问题
在频繁修改的时候会产生大量拷贝,使得我们的代码 TLE
因此就要选择 OIer 们最熟悉的 `可变 Vector`
但可变 `Vector` 因为是可变的,所以就变得不纯了
这里又要请到 `IO Monad` 了
首先导入 Data.Vector.Unboxed 的 Mutable 版本
```haskell
import qualified Data.Vector.Unboxed.Mutable as MV
import Data.Vector.Unboxed.Mutable (IOVector)
```
### 构造 Vector
可变 `Vector` 的构造方式就没有 不可变 `Vector` 那么多样了,只提供了 `new` 和 `repilicate`
```haskell
> :i MV.new
MV.new ::
(Control.Monad.Primitive.PrimMonad m, MV.Unbox a) =>
Int -> m (MV.MVector (Control.Monad.Primitive.PrimState m) a)
-- Defined in ‘Data.Vector.Unboxed.Mutable’
```
函数签名有些复杂,其实就是接收一个长度 $n$,并返回一个长度为 $n$ 的 `Vector`
```haskell
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
return ()
```
### 访问 Vector
可变 `Vector` 可以使用 `read` 函数来进行随机访问
```haskell
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
v <- MV.read vec 0 -- 读取 vec 中下标为 0 的元素
print v
return ()
```
输出了 0
时间复杂度是 $O(1)$
### 写入 Vector
可变 `Vector` 可以使用 `write` 函数来进行随机写入
```haskell
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
MV.write vec 0 1 -- 对 vec 中下标为 0 的元素赋值 1
v <- MV.read vec 0 -- 读取 vec 中下标为 0 的元素
print v
return ()
```
输出了 1
时间复杂度是 $O(1)$
### 修改 Vector
可变 `Vector` 可以使用 `modify` 函数来进行修改
```haskell
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
MV.modify vec (+1) 0 -- 对 vec 中下标为 0 的元素应用 (+1) 函数
v <- MV.read vec 0 -- 读取 vec 中下标为 0 的元素
print v
return ()
```
输出了 1
时间复杂度是 $O(1)$
### 扩容 Vector
如果大小不够用了,可以使用 `grow` 函数来扩大 `Vector` 的容量
```haskell
main :: IO ()
main = do
vec <- MV.new 5 :: IO (IOVector Int)
print $ MV.length vec
vec <- MV.grow vec 1 -- 把 vec 扩大 1,然后返回
print $ MV.length vec
return ()
```
输出 5 和 6
# 变量
不!!!没有变量我要死了。。。
可能很多 OIer 都会这样子
虽然 Haskell 中没有变量,但提供了一个数据结构
可以实现类似变量的操作
当然,依然使用了 `IO Monad`
首先导入 `Data.IORef` 包
```haskell
import Data.IORef
```
## 创建 IORef
使用 `newIORef` 来创建一个变量
```haskell
-- newIORef :: a -> IO (IORef a)
main :: IO ()
main = do
v <- newIORef 1
return ()
```
## 读取 IORef
使用 `readIORef` 来读取变量中的值
```haskell
-- readIORef :: IORef a -> IO a
main :: IO ()
main = do
v <- newIORef 1
readIORef v >>= print
return ()
```
## 写入 IORef
使用 `writeIORef` 来向变量中写入值
```haskell
-- writeIORef :: IORef a -> a -> IO ()
main :: IO ()
main = do
v <- newIORef 1
writeIORef v 2
readIORef v >>= print
return ()
```
## 修改 IORef
使用 `modifyIORef` 来修改变量中的值
相当于 `readIORef` 和 `writeIORef` 的结合
```haskell
-- modifyIORef :: IORef a -> (a -> a) -> IO ()
main :: IO ()
main = do
v <- newIORef 1
modifyIORef v (+1)
readIORef v >>= print
return ()
```
虽然 Haskell 提供了 `IORef`,但我并不支持滥用 `IORef` 的行为 ~~(可以无视我说的话)~~
# 映射表
映射表也是在 OI 中常用的一种数据结构
映射表被包含在 [container 库](http://hackage.haskell.org/package/containers) 中,在 Data.Map 包下
由于部分函数命名冲突,需要使用 `qualified`
```haskell
import qualified Data.Map as M
```
## 构造映射表
可以使用 `empty` 来构造一个空表
`singleton` 来构造包含一个映射的表
`fromList` 从 `(键, 值)` 映射的列表中构造映射表
```haskell
> M.empty
fromList []
> M.singleton 1 2
fromlist [(1,2)]
> M.fromList [(1, 2)]
fromlist [(1,2)]
```
## 添加映射
可以使用 `insert` 函数来添加映射
```haskell
> :i M.insert
M.insert :: Ord k => k -> a -> M.Map k a -> M.Map k a
-- Defined in ‘Data.Map.Internal’
> M.insert 1 2 M.empty
fromList [(1,2)]
```
## 访问映射表
使用 `(!)` 或者 `(!?)` 来访问映射表
```haskell
> import Data.Map ((!), (!?))
> :i (!) (!?)
(!) :: Ord k => M.Map k a -> k -> a
-- Defined in ‘Data.Map.Internal’
(!?) :: Ord k => M.Map k a -> k -> Maybe a
-- Defined in ‘Data.Map.Internal’
> M.empty ! 1
*** Exception: Map.!: given key is not an element in the map
CallStack (from HasCallStack):
error, called at libraries\\containers\\Data\\Map\\Internal.hs:610:17 in containers-0.6.0.1:Data.Map.Internal
> M.empty !? 1
Nothing
```
使用 `(!)` 访问映射表,如果映射不存在就会抛出一个错误
而 `(!?)` 则是返回一个 `Maybe` 类型
# 栈
栈可以使用 Haskell 的原生列表实现
```haskell
pop :: [a] -> ([a], a)
pop (x:xs) = (xs, x)
push :: a -> [a] -> [a]
push = (:)
top :: [a] -> a
top = head
```
还可以使用 State Monad 来更优雅地实现,但此处不过多介绍
# 队列
队列可以使用两个栈的方法来模拟,这里提供我自己实现的队列
```haskell
-- Queue 输出端 输入端
data Queue a = Queue [a] [a]
instance (Show a) => Show (Queue a) where
show (Queue o i) = show $ o ++ reverse i
front :: Queue a -> a
front (Queue [] xs) = last xs
front (Queue xs _) = head xs
push :: Queue a -> a -> Queue a
push (Queue o i) v = Queue o (v:i)
pop :: Queue a -> Queue a
pop (Queue [] []) = error "empty queue"
pop (Queue [] xs) = pop $ Queue (reverse xs) []
pop (Queue (_:xs) ys) = Queue xs ys
fromList :: [a] -> Queue a
fromList xs = Queue xs []
```
# 邪门优化
## 严格求值
Haskell 是惰性的,但惰性求值会影响尾递归的优化
所以我们需要添加一个扩展,使得 Haskell 严格求值
在文件开头添加这样一句话:
```haskell
{-# LANGUAGE Strict #-}
```
## 吸氧
Haskell 也有 O2 优化,在文件开头添加这样一句话:
```haskell
{-# OPTIONS_GHC -O2 #-}
```
# Hoogle
Hoogle 是 Haskell 生涯中很重要的工具
[Hoogle](https://hoogle.haskell.org/) 是 Haskell 官方的函数文档
你可以在这里面查找函数的签名和用法,包括源代码实现
或者查找各种各样的依赖库
# 更多
如果对文章内容有疑惑~~,或者想喷我菜的~~,都可以在评论区留言,我会尽可能回复
---------------------------
参考: [并查集讨论](https://www.luogu.org/discuss/show/125819)