Haskell学习笔记(未完待续)

这篇文章是我Haskell的学习笔记,内容来自《Learn You a Haskell for Greate Good》中文版。

# 1 起步

使用ghci打开REPL交互环境。可以输入表达式常看其结果。通过:l module可以载入模块。

支持的运算符有一元负号-(唯一的一元运算符),双目运算符有算术+-*/(参数为Num类),关系==、不等于/=(参数为Eq类)等等。

如果表达式存在负数常量,最好用括号括起来(5 * -3会报错,5 * (-3)则不会)。对于5之类的数,它既可以是整数也可以是浮点数。

函数有中缀函数(运算符)和前缀函数。调用前缀函数的格式为func arg1 arg2 ...。函数应用拥有最高优先级。二元函数可以使用中缀函数的形式调用arg1 `func` arg2

函数定义的格式为func param1 param2 ... = expression。函数定义没有顺序的概念。函数名可以包含一些符号,但不能大写开头。函数一经定义,不能改变。

有如下函数:

  1. succpred:一元,获取后继和前趋,参数为Enum类;
  2. minmax:二元,获取最小和最大值,参数为Ord类;
  3. divmod:二元,整数取整和求余,参数为Integral类。

if表达式形如if boolExpr then expr1 else expr2,else不可省略。

列表是单类型的数据结构。使用[elem1,elem2,..,elemN]的方式定义列表。字符串是列表的语法糖。使用list ++ list可以连接列表,其复杂度正比于左边列表的长度。使用elem:list可插入元素到列表头部,[1, 2, 3]实际上是1:2:3:[]的语法糖。使用list !! index可以索引元素(index为Int类型)。列表可以嵌套,但类型必须一致。列表可以比较,采用字典序。

列表有以下函数:

  1. head list:返回list的第一个元素;
  2. tail list:返回list中除第一个元素的剩余部分(是一个列表);
  3. init list:返回list中除最后一个元素的剩余部分(是一个列表);
  4. last list:返回list的最后一个元素;
  5. length list:返回list的长度(Int类型);
  6. null list:返回list是否为空;
  7. reverse list:返回翻转的list
  8. take num list:返回list中的前num个元素(列表长度可小于num);
  9. drop num list:返回list删除前num个元素的结果(列表长度可小于num);
  10. maximum listminimum list:返回最大最小值;
  11. sum listproduct list:返回加和和乘积;
  12. elem value list:返回value是否在list中,通常以中缀函数调用。

可以使用[elem1..elemN]产生一个区间,其元素为Enum,如果需要更改步长,可以采用[elem1,elem2..elemN]。使用[elem1..][elem1,elem2..]可创建无穷列表,可以充分利用Haskell的惰性求值。注意浮点数可能有误差。

有以下的生成列表的函数:

  1. cycle list:创建一个循环list的无穷列表;
  2. repeat elem:创建一个循环elem的无穷列表;
  3. replicate count elem:创建一个将elem循环count次的列表。

使用[expr | bindOrPredict, ...]可以创建列表推导式,其中bindOrPredict为形如name <- list的绑定或Bool表达式的谓词,expr和谓词都可以含有绑定创建的名字,可以有多个谓词和绑定,可以嵌套。

元组将多个不同类型的值合成为一个值。使用(elem1,elem2,..,elemN)的方式定义元组。不同长度、不同元素类型的元组都是不同的类型。元组可以比较。元组是固定大小的。不存在单元素的元组。

元组有以下函数:

  1. fst tuplesnd tupletuple为二元组,分别返回第一个元素和第二个元素;
  2. zip list1 list2:将两个列表交叉配对,返回元素为二元组的列表,较长的列表会被截断。

# 2 类型和类型类

在GHCi使用:t expr查看表达式的类型。列表的类型为[type],元组的类型为(type1, ... , typeN)。可以给函数显示的类型声明,形如func :: argType1 -> ... -> argTypeN -> retType。函数类型中可以包含类型变量(小写开头),这样的函数称为多态函数

有如下基本类型:

  1. Int:有界的与机器相关的整数;
  2. Integer:无界的高精度整数;
  3. Float:单精度浮点数;
  4. Double:双精度浮点数;
  5. Bool:布尔值;
  6. Char:Unicode字符。

类型类定义接口。一个类型可以是类型类的实例。带类型类的函数签名形如func :: (TypeClass1 typeVar1, ..., TypeClassN typeVarN) => argType -> ... -> retType,其中=>左侧的东西称为类型约束

有如下类型类:

  1. Eq:定义了==/=
  2. Ord:是Eq的子类型类,还定义了<><=>=compare比较两个Ord类型类的值返回Ordering类型的结果,它有3个值GTLTEQ
  3. Show:定义了show(将参数转为字符串);
  4. Read:定义了read(将字符串参数转换为实例,一般需要配合类型注解,形如expr :: Type);
  5. Enum:定义了predsucc,包含()BoolCharOrderingIntIntegerFloatDouble
  6. Bounded:定义了多态常量minBoundmaxBound,如果元组的每个元素类型都属于Bounded的实例,那么该元组的类型也是Bounded实例;
  7. Num:是Show和Eq的子类型类,定义了数值运算,包含IntIntegerFloatDouble
  8. Floating:包含了FloatDouble
  9. Integral:包含了IntInteger,可以通过fromIntegral将Integral类型类的实例转换为Num类型类的实例。

# 3 函数的语法

模式匹配可以检查数据是否匹配模式,并从模式中解析出数据。其形式类似额外的函数定义,只不过参数不再是单一变量。模式匹配自上而下,通常结尾包含万能模式。除了函数定义之外,列表推导式中的绑定也可以使用模式匹配。失败的模式匹配是个运行错误。有以下的几种匹配:

  1. 常量:参数为常量,匹配常量;
  2. 元组:参数为(var1, ..., varN),匹配一个元组;
  3. 列表:参数为x1:...:xN:xs,其中xs也可以为[],匹配一个列表的前几个(或全部)元素。

此外还有特殊的模式,叫as模式,形如var@pattern,可以保留整体引用。

模式是检查参数结构是否匹配,哨卫检查参数的性质是否为真。其形式如下:

func params
    | predict1 = expr1
    ...
    | predictN = exprN
    | otherwise = expr

如果所有哨卫都没通过,且没有otherwise作为万能条件,就转入下一个模式。其实otherwise就是True

使用where绑定可以将定义置于函数的末尾,也可以跟在哨卫的后面。形如:

func params = expression
    where var1 = expr1
          ...
          varN = exprN

其名字的作用域只对函数可见,名字必须对齐在同一行。where绑定中也可以使用模式匹配。

使用let表达式可以定义局部变量(或函数),不能与哨卫配合使用。形如let bindings in expr。也可以使用模式匹配。在列表推导式中,可省略in绑定局部变量。GHCi中letin部分也可以省略,名字会在整个回话中存在。如果bindings有多个,可以采用分号分隔,末尾分号可选。此外列表推导式也可以使用不带in字句的let表达式。

使用case表达式可以对表达式进行模式匹配,形如:

case expression of pattern1 -> result1
                   ...
                   patternN -> resultN

函数参数的模式匹配是case表达式的语法糖。

# 4 递归

递归在Haskell中有广泛的应用。

zip函数接受两个列表,返回一个元素是二元元组的列表,较长的列表会从中间断开。

# 5 高阶函数

Haskell的所有函数都是柯里函数(只有一个参数)。函数签名中的->是右结合的。可以以部分参数调用某函数,得到一个部分应用。通过截断,可以对中缀函数进行部分应用。将一个参数放在中缀函数的一侧,即可截断中缀函数。用括号括住一个二元运算符即可得到二元函数。

有以下函数:

  1. zipWith func list1 list2:接受一个二元函数和两个列表,返回一个列表,其元素是两个列表的元素作为func参数的结果;
  2. takeWhile func listfunc是谓词,返回满足funclist的最长前缀;
  3. flip func:接受一个二元函数,返回一个二元函数,交换它的两个参数;
  4. map func list:接受一个一元函数和一个列表,返回一个列表,其元素是list的元素作为func参数后的结果;
  5. filter func list:接受一个谓词和一个列表,返回一个列表,其元素为list元素作为func参数结果为真的那些参数。

lambda函数形如\param1 ... paramN -> expr,可以创建一个临时的函数。

Haskell有折叠函数,将列表归约为单个值。左折叠无法处理无限列表,右折叠可以。有如下函数:

  1. foldl func init list:左折叠,func为二元函数,第一个为累加值,第二个为list的元素;
  2. foldr func init list:右折叠,func为二元函数,第一个为list的元素,第二个为累加值;
  3. foldl1 func listfoldr1 func list:左折叠和右折叠,无初始值;
  4. scanl func init listscanr func init list:左扫描和右扫描,与折叠类似,不过值会被累积地记录在一个列表中;

函数应用符$的定义为f $ x = f x,右结合,具有低优先级,可以用来减少括号。

函数组合.的定义为f . g = \x -> f (g x),右结合。

# 6 模块

导入模块的语法为import module。在GHCi中,可以通过:m + module1 ... moduleN。如果只导入一个模块的某几个函数,可以用import module (func1, ..., funcN)。如果导入一个模块除某些函数之外的函数,可以用import module hiding (func1, ..., funcN)。如果希望导入的东西都需要模块名前缀,即限定导入,可以用import qualified module,也可以设置别名import qualified module as alias

Data.List模块里有以下函数:

  1. words str:以空白符为分隔,将str切割成字符串列表;
  2. unwords strList:以空格为分隔,合并字符串列表;
  3. group list:返回列表的列表,将相邻的相同元素组合成一个列表;
  4. sort list:给list排序
  5. inits listtails list:返回列表的列表,从左到右依次返回前缀的列表和后缀的列表;
  6. list1 `predict` list2,其中predictisPrefixOfisInfixOfisSuffixOf:判断是否为前缀、中缀和后缀;
  7. foldl' func init listfoldl1' func list:严格左折叠,不延迟计算;
  8. find predict list:返回Maybe类型,表示是否在list中找到predict为真的元素。

Data.Char模块里有以下函数:

  1. ord char:返回char的Unicode码;
  2. chr num:返回num对应的Unicode字符;
  3. digitToInt char:返回16进制字符char对应的数字;
  4. isDigit:返回字符是否是十进制数字。

我们用Maybe a类型来表示一个可以为空或包含一个元素的类型。如果为空,就用Nothing,如果不为空就用Just value

Data.Map模块提供了映射的数据结构Map k a,一般使用限定引入。它提供了以下函数:

  1. fromList listlist是关联列表(其元素是键值对元组),构建映射(重复键中的旧键会被忽略);
  2. lookup key map:在映射中查找键,返回Maybe包装的值;
  3. insert key value map:向映射插入键值对;
  4. size map:返回映射的大小;
  5. fromListWith func listfunc接受两个值,返回一个值,list是关联列表,构建映射,如果有重复的键,调用func

通过以下方式导出模块中的函数:

module name
( func1
, func2
...
, funcN
) where

其中name也可以是path.namename必须与文件名对应,path.分隔的路径,必须与文件夹名称对应。

# 7 构造我们自己的类型和类型类

使用data Type = DataCtor1 Params1 | ... | DataCtorN ParamsN定义自定义类型。其中DataCtor1DataCtorN值构造器,可以有任意参数(包括无参数),它与普通函数没有差别。Params1ParamsN是空格分隔的类型列表,表示值构造器的参数的类型。

可以使用DataCtor arg1 ... argN进行模式匹配。

在导出类型时,使用Type(DataCtor1, ..., DataCtorN)导出某几个值构造器,使用Type(..)导出所有值构造器,也可以不带括号,不导出值构造器。

使用记录语法可以方便地处理多字段的情况,格式如下:

data Type = DataCtor { field1 :: Type1
                     , ...
                     , fieldN :: TypeN }

这会自动生成提取字段的函数,如field1fieldN。可以这样构建值,DataCtor {field1=value1, ..., fieldN=valueN},这也可以作为模式匹配。

自定义类型也可以带有类型参数,形如data Type a b ...,这里Type不是类型,而是类型构造器。填满类型构造器的所有参数就可以得到类型。可以在声明中添加类型约束,形如data (TypeClass1 typeArg1, ...) => Type a b ... = ...,但请永远不要那么做。

注意:如果一个类型没有参数,或参数填满,则称这个类是具体的。凡是值的类型都是具体的。

值构造器和类型构造器可以同名,但可以根据上下文区分,请勿搞混。

在自定义类型后面添加deriving (TypeClass1, ..., TypeClassN),可以将该类型作为类型类的实例。诸如Eq(先检查值构造器是否一致,再检查字段是否一致)、Show、Read、Ord(先比较值构造器,越靠前越小,再比较字段)、Enum、Bounded类型类都可以。

通过type NewType = SomeType可以创建类型别名,也可以带有类型参数,形如type NewType a b = ...。类型别名也可以formula归约。

除了Maybe外,Either也经常用于错误处理,形式如下,错误使用Left,正确使用Right

data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

还可以创建递归数据结构,这被用于构建列表和树。

通过下面所示的语法创建类型类,函数可以没有实现。许多类型类的函数互相调用,从而有最小的实现方式。

class TypeClassName typeParam where
    funcName :: funcType
    funcName params = ...
    ...

实现类型类的语法如下所示。

instance TypeClassName TypeName where
    funcName params = ...

也可以将一个类型类实现为另一个类型类的子类,这时语法形如class (TypeClass type) => SubTypeClass type where ...。类型类实例的类型也可以带参数,以Eq Maybe举例,形如instance (Eq m) => Eq (Maybe m) where

通过GHCi中输入:info TypeClass:info Type(或类型构造器)可以查看实现的类型类。、

Functor类型类定义如下:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

fmap函数取一个函数和封装的数据,将函数应用到被封装的数据,返回结果。数组、Maybe、树、Either a都是函子。

类型有kind,类似于值有类型,可以再GHCi输入:k Type查看。

# 8 输入与输出

像诸如putStrLn返回的类型是IO (),其中()是空元组。可以通过do将多个I/O操作合并成为一个操作,以putStrLngetLine为例,如下:

main = do
    putStrLn "Hello, what's your name?"
    name <- getLine
    ...

第3行的操作叫绑定名字,它从容器中取出值,绑定到指定的名字。不能为最后一个操作绑定名字。与列表推导式类似,可以在do语句块中使用letreturn取一个值作为参数,并将它包装到容器中。因此return ()的类型就是IO ()return不会中断do代码块的执行。可以通过name <- return value来绑定名字,当然使用let更好。

有如下几个实用的I/O函数:

  • putStrLn:打印字符串并换行;
  • putStr:打印字符串不换行;
  • putChar:打印字符不换行,可以借此递归实现出putStr
  • print:等价于putStrLn . show
  • when:定义于Control.Monad,取一个布尔值和一个I/O操作,如果布尔值为真则返回I/O操作,否则返回return ();
  • sequence:接受一个I/O操作的列表,返回一个I/O操作,其内容是所有参数I/O操作执行后结果组成的列表;
  • mapM:接受一个返回I/O操作的函数,和一个列表,将函数应用到列表的每一个元素上,再调用sequence,如果不关心返回值,可以调用mapM_
  • forever:接受一个I/O操作,返回一个永远重复执行该I/O操作的I/O操作;
  • forM:与mapM相似,但参数顺序相反。

# 9 更多的输入输出操作

使用getContents读入所有字符直到遇到EOF,它是惰性的。interact接受一个字符串到字符串的函数,返回一个I/O操作,读入,应用函数并输出。

(未完待续)

2016-2020 Ziping Sun
京ICP备 17062397号