Learning Haskell Note - 01
这学期上了一门《程序语义分析与验证》的课,老实说我对其中的数学证明不太感冒(对我来说,有点“过于”严谨了),但是其中介绍的函数式编程语言非常吸引我,打算趁这个机会玩一玩。
这个系列不会用来记录繁杂的知识点,而是写下一些偶尔的想法。
函数的柯里化
在编写”根据key查找map中数据”的时候,刚开始我编写了基础版本的程序:
1
2
3
4
book = [("a", 12), ("b", 13), ("c", 14), ("d", 15)]
findKey k m =
snd (head (filter (\x -> fst x == k) m))
然而尝试引入 .的时候,遇到了一些错误:
1
2
findKey k m =
snd . head (filter (\x -> fst x == k) m)
• Couldn’t match expected type: (a1, b0) with actual type: a2 -> (a3, c) • Probable cause: ‘x’ is applied to too few arguments
问题出在结合顺序上,对于.运算符,需要结合两个函数,但是head和后面的参数结合后,结果不再是函数,而是一个tuple.
解决方法:
1
2
3
4
5
findKey k m =
(snd . head) (filter (\x -> fst x == k) m)
-- 或者下面的方法
findKey k m =
snd . head $ (filter (\x -> fst x == k) m)
$运算符能够将左侧和右侧“阻断”,从而将左侧解释为一个复合函数,右侧是一个list
如果想要进一步使用.来结合,下面的方法会报错:
1
2
findKey k m =
snd . head . filter (\x -> fst x == k) m
• Couldn’t match expected type: a2 -> [(a0, c)] with actual type: [(a1, b)] • Possible cause: ‘filter’ is applied to too many arguments
理解错误很容易,可是要如何修改呢?如果仿照上面的例子改成:
1
2
findKey k m =
snd . head . filter $ (\x -> fst x == k) m
还是有问题,仔细想想:要结合 f . g,必须要求g的返回值类型和f的参数类型相符合。 如果把filter理解成“接受两个参数,返回一个list”的函数,肯定会想:没有什么问题啊?这不就是head所需要的吗?
然而,在haskell中,所有的函数其实本质上都是“接受一个参数,返回一个结果”,对于filter,本质上是“接受一个函数(用来判定),返回一个函数B”,其中函数B进一步才有”接受一个list, 返回一个list”, 这也是为什么我们习惯了用“多参数”的视角看待问题后,很容易出现结合顺序的问题。对于f x y,实际上相当于(f x) y, 因此有时候写f -1会导致( f (-) ) 1而不是f (-1),进而语法报错。
理解了这一点后,就可以找到一种解决方法:
1
2
findKey k m =
snd . head . filter (\x -> fst x == k) $ m
这里filter优先以后面的lambda表达式作为参数,返回一个“函数B”,而这个函数B会“接受一个list, 返回一个list”,接着我们用这个函数B参与和snd . head的复合。
haskell基础开发环境
在nvim打开.hs文件的时候遇到报错: Failed to find a HLS version for GHC 9.10.1 … 于是决定研究一下基本的hs开发环境.
ghc & hls安装位置
两个重要的软件包:
ghc(Glasgow Haskell Compiler) hls(Haskell Language Server)
查看版本:
1
2
3
4
(venv14) woc@myarch:study/haskell $ ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.10.1
(venv14) woc@myarch:study/haskell $ haskell-language-server-wrapper --version
haskell-language-server version: 2.12.0.0 (GHC: 9.10.3) (PATH: /home/woc/.ghcup/hls/2.12.0.0/lib/haskell-language-server-2.12.0.0/bin/haskell-language-server-wrapper)
先说ghc. 一台机器上可以同时安装多个版本的ghc, 最基本的方式是使用ghcup进行安装/选择:
1
2
3
ghcup install ghc 8.10.7
ghcup install ghc 9.6.5
ghcup set ghc 9.6.5
hls必须和当前选择的ghc版本匹配,否则无法正常工作(比如打开nvim的时候弹出的提示). 在我的系统上,haskell的这些工具都安装在~/.ghcup下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(venv14) woc@myarch:~/.ghcup $ ls
bin cache config.yaml db env ghc hls logs share tmp trash
(venv14) woc@myarch:~/.ghcup $ tree -L 2 ghc
ghc
├── 9.10.1
│ ├── bin
│ ├── lib
│ └── share
├── 9.10.3
│ ├── bin
│ ├── lib
│ └── share
├── 9.4.8
│ ├── bin
│ ├── lib
│ └── share
└── 9.6.7
├── bin
├── lib
└── share
17 directories, 0 files
(venv14) woc@myarch:~/.ghcup $ tree -L 2 hls
hls
├── 2.10.0.0
│ ├── bin
│ └── lib
├── 2.11.0.0
│ ├── bin
│ └── lib
├── 2.12.0.0
│ ├── bin
│ └── lib
└── 2.13.0.0
├── bin
└── lib
13 directories, 0 files
首先要注意的是:系统中是存在多个ghc / hls目录的.
另外可以看到一个hls版本目录下,对应了多个ghc版本的language server. 而haskell-language-server-wrapper同时支持这么多个不同的版本,如果系统的ghc在这个范围内,会自动使用相应的hls.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(venv14) woc@myarch:~/.ghcup $ tree -L 2 hls/2.11.0.0
hls/2.11.0.0
├── bin
│ ├── haskell-language-server-9.10.2
│ ├── haskell-language-server-9.12.2
│ ├── haskell-language-server-9.4.8
│ ├── haskell-language-server-9.6.7
│ ├── haskell-language-server-9.8.4
│ └── haskell-language-server-wrapper -> ../lib/haskell-language-server-2.11.0.0/bin/haskell-language-server-wrapper
└── lib
└── haskell-language-server-2.11.0.0
4 directories, 6 files
(venv14) woc@myarch:~/.ghcup $ tree -L 2 hls/2.12.0.0
hls/2.12.0.0
├── bin
│ ├── haskell-language-server-9.10.3
│ ├── haskell-language-server-9.12.2
│ ├── haskell-language-server-9.6.7
│ ├── haskell-language-server-9.8.4
│ └── haskell-language-server-wrapper -> ../lib/haskell-language-server-2.12.0.0/bin/haskell-language-server-wrapper
└── lib
└── haskell-language-server-2.12.0.0
4 directories, 5 files
而当前系统中使用的haskell-language-server-wrapper就指向了众多hls目录中的某一个:
1
2
3
4
(venv14) woc@myarch:~/.ghcup $ which haskell-language-server-wrapper
/home/woc/.ghcup/bin/haskell-language-server-wrapper
(venv14) woc@myarch:~/.ghcup $ ll ~/.ghcup/bin/haskell-language-server-wrapper
lrwxrwxrwx 1 woc woc 40 Dec 26 00:43 /home/woc/.ghcup/bin/haskell-language-server-wrapper -> haskell-language-server-wrapper-2.12.0.0
hls支持的版本
搞清楚上面的东西后,重新审视一下当前的环境:
1
2
3
4
5
6
7
8
9
10
11
(venv14) woc@myarch:~/.ghcup $ ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.10.1
(venv14) woc@myarch:~/.ghcup $ haskell-language-server-wrapper --version
haskell-language-server version: 2.12.0.0 (GHC: 9.10.3) (PATH: /home/woc/.ghcup/hls/2.12.0.0/lib/haskell-language-server-2.12.0.0/bin/haskell-language-server-wrapper)
(venv14) woc@myarch:~/.ghcup $ ll ~/.ghcup/hls/2.12.0.0/bin
total 52
-rwxr-xr-x 1 woc woc 9050 Dec 21 12:09 haskell-language-server-9.10.3
-rwxr-xr-x 1 woc woc 9257 Dec 21 12:09 haskell-language-server-9.12.2
-rwxr-xr-x 1 woc woc 8651 Dec 21 12:09 haskell-language-server-9.6.7
-rwxr-xr-x 1 woc woc 8667 Dec 21 12:09 haskell-language-server-9.8.4
lrwxrwxrwx 1 woc woc 75 Dec 21 12:09 haskell-language-server-wrapper -> ../lib/haskell-language-server-2.12.0.0/bin/haskell-language-server-wrapper
显然当前的hls版本目录下没有目标的9.10.1.
通过ghcup install hls后,给我升级到了最新的hls-2.13.0, 但是经过检查其下面仍然没有9.10.1的目录.
后来查阅资料发现,每个hls版本支持什么版本的ghc是已经确定的,可以在release中查看:
https://github.com/haskell/haskell-language-server/releases
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 2.13.0
9.6.7
9.8.4
9.10.3
9.12.2
9.14.1
# 2.12.0
9.6.7
9.8.4
9.10.3
9.12.2
# 2.11.0
9.4.8
9.6.7
9.8.4
9.10.2
9.12.2
# 2.10.0
9.4.8
9.6.7
9.8.4
9.10.1
9.12.2
可以看到:最新版本的hls确实删去了对ghc-9.10.1的支持.
所以有两个解决方法:
- 将ghc升级到9.10.3
1 2
ghcup install ghc 9.10.3 ghcup set ghc 9.10.3
- 安装2.10.0版本的hls:
1 2
ghcup install hls 2.10.0.0 ghcup set hls 2.10.0.0
haskell导入包
使用import导入package. 但是经常会遇到函数名冲突的情况, 比如:
1
2
import Data.Map
xs = map (+1) [1,2,3] -- ambiguous! Which `map`?
这种情况下可以使用限定导入(qualified import):
1
2
3
import qualified Data.Map
xs = map (+1) [1,2,3] -- Prelude.map, unambiguous
dict = Data.Map.fromList [(1,"a")] -- must use the full prefix
限定导入的模块,在使用的时候需要加上完整的前缀. 当然,如果前缀很长,使用的时候会很不方便,所以还提供了alias功能:
1
2
3
4
5
import qualified Data.Map as Map
import qualified Data.Set as Set
dict = Map.fromList [(1, "a"), (2, "b")]
val = Map.lookup 1 dict
s = Set.fromList [1, 2, 3]
总结:
1
2
3
4
5
import Data.Map -- everything unqualified (risky if clashes)
import Data.Map (fromList, lookup) -- only these names, unqualified
import qualified Data.Map as M -- everything, but only as M.foo
import qualified Data.Map as M (fromList) -- only fromList, as M.fromList
import Data.Map hiding (lookup) -- everything except lookup
基础IO
IO作为“不纯”的操作,在haskell中有意地和那些没有副作用的操作进行区分.
IO行为
首先,在haskell中,main函数被定义为一个IO action:
1
main :: IO ()
这意味这它不能接受String类型的返回值,而是需要类似IO String的类型.
我们可以通过<-运算符将一个IO行为的结果绑定到一个变量上,获得其具体的值:
1
2
getLine :: IO String -- no args, returns "IO String"
content <- getLine -- type of content is "String"
整个程序只触发一次IO操作,但是我们可以通过do关键词将多个IO操作进行合并:
1
2
3
4
main = do
putStrLn "hello"
name <- getLine
putStrLn name
return
haskell中的return并没有终止的含义,它唯一的作用就算构造一个返回某个值的IO操作:
1
2
3
4
main = do
return ()
return "abc"
putStrLn "Hello World"
()类型经过return,会产生一个IO ()的类型,可以把这一行的行为当作getLine, 只不过return语句前面并没有变量将其绑定,所以值被忽略了.
return语句本身可以视为执行一个函数(这个函数并不会触发exit),而是正常的执行完,然后继续下一行代码.
随机数
安装库:
1
2
cabal update
cabal install --lib random
然后可以import System.Random
1
2
3
4
ghci> :t random
random :: (Random a, RandomGen g) => g -> (a, g)
ghci> :t mkStdGen
mkStdGen :: Int -> StdGen
mkStdGen接受一个数字作为seed,然后返回一个随机数生成器,而random接受一个随机数生成器g, 返回一个二元组,包含生成的随机数,以及一个更新的随机数生成器:
1
2
ghci> random (mkStdGen 100)
(9216477508314497915,StdGen {unStdGen = SMGen 712633246999323047 2532601429470541125})
字节串
列表虽然方便,但是面对大容量的数据吞吐时(比如文件读写、网络数据传输),效率很差. 这种情况下,我们可以使用ByteString, 也就是原始的字节数据类型.
import的对象是Data.ByteString, 搜索关键字找到Hoogle上的页面,标题写着:bytestring-0.12.2.0: Fast, compact, strict and lazy byte strings with a list interface, 这意味着这个包的名字是bytestring,接着可以通过cabal安装.
ByteString提供了Strict和Lazy版本:
1
2
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
pack将一个数据列表打包成字节串:
1
2
ghci> :t BS.pack
BS.pack :: [ghc-internal:GHC.Internal.Word.Word8] -> B.ByteString
其中Word8要求列表的每个元素都在0-255范围内. 如:
1
2
ghci> BS.pack [80..100]
"PQRSTUVWXYZ[\\]^_`abcd"