[关闭]
@delight 2017-12-05T20:27:35.000000Z 字数 11377 阅读 5255

重拾Haskell

Haskell FP


我打算学习但是最终没能完全理解的语言大概有三门:因为复杂性而放弃的Scala和Rust,因为难以理解而放弃的Haskell. 学习过C++以后,我对太过复杂的语言一项是敬而远之的,这一点暂时不会有变化,以后在工作中如果遇到必须使用Scala的时候我还是会认真学习。但是对于Haskell还是有些不甘心。虽然这是一门号称不看论文很难理解的语言,我还是想尽力学习一下。

Haskell相对于其他语言,变化的速度非常快,现在最新的平台是stack,连Haskell Platform也过时了(有种web前端的感觉),不过其核心理念还是比较稳定的,以前从Haskell趣学指南中学习的东西,现在也大多还能用。

apt-get install haskell-stack以后,使用stack upgrade升个级,stack setup安装环境,使用stack ghci打开交互式环境,然后就可以愉悦的学习了。目前最的版本是ghc-8.0.1;

优化显示

默认的stack使用起来不是很方便,有些地方可以优化一二。
首先编辑~/.stack/config.yaml,删掉空白的{},加入以下内容:

  1. templates:
  2. params:
  3. author-email: yaotairan@gmail.com
  4. author-name: tryao
  5. category: Personal
  6. copyright: 'Copyright (c) TairanYao'
  7. github-username: YiuTerran
  8. setup-info: "http://mirrors.tuna.tsinghua.edu.cn/stackage/stack-setup.yaml"
  9. urls:
  10. latest-snapshot: http://mirrors.tuna.tsinghua.edu.cn/stackage/snapshots.json
  11. lts-build-plans: http://mirrors.tuna.tsinghua.edu.cn/stackage/lts-haskell/
  12. nightly-build-plans: http://mirrors.tuna.tsinghua.edu.cn/stackage/stackage-nightly/
  13. package-indices:
  14. - name: Tsinghua
  15. download-prefix: http://mirrors.tuna.tsinghua.edu.cn/hackage/package/
  16. http: http://mirrors.tuna.tsinghua.edu.cn/hackage/00-index.tar.gz

这是stackage的修改
上面的email和名字之类的需要换成自己的,后面加了清华的源,加快cabal的下载速度。

编辑~/.ghci加入以下内容:

  1. import qualified IPPrint
  2. import qualified Language.Haskell.HsColour as HsColour
  3. import qualified Language.Haskell.HsColour.Colourise as HsColour
  4. import qualified Language.Haskell.HsColour.Output as HsColour
  5. let myColourPrefs = HsColour.defaultColourPrefs { HsColour.conid = [HsColour.Foreground HsColour.Yellow, HsColour.Bold], HsColour.conop = [HsColour.Foreground HsColour.Yellow], HsColour.string = [HsColour.Foreground HsColour.Green], HsColour.char = [HsColour.Foreground HsColour.Cyan], HsColour.number = [HsColour.Foreground HsColour.Red, HsColour.Bold], HsColour.layout = [HsColour.Foreground HsColour.White], HsColour.keyglyph = [HsColour.Foreground HsColour.White] }
  6. let myPrint = putStrLn . HsColour.hscolour (HsColour.TTYg HsColour.XTerm256Compatible) myColourPrefs False False "" False . IPPrint.pshow
  7. :set -interactive-print=myPrint
  8. :set -XNoMonomorphismRestriction
  9. :set prompt "λ "

然后在shell里运行:

  1. sudo apt-get install libbz2-dev
  2. stack install ipprint hscolour

执行stack安装依赖时,可能会报错,根据提示修正即可.
这是给ghci添加语法高亮.

最后,如果用zsh的话, 可以打开stack插件,方便自动完成. 也可以alias ghci='stack ghci'. 另外,最好把~/.local/bin加到PATH里面去.

基础

仍然从《Haskell趣学指南指南》这本书开始是比较合适的,虽然内容有些过时,但是基础部分变动应该不大。上次看这本书的时候我还不会python呢,如今js我也驾轻就熟了,看起来应该简单很多了。完成后开始看《Real World Haskell》,后者相关习题的个人解答:https://github.com/YiuTerran/rwh-exercise

语法

基本语法大体和普通语言(C系)相似,除了:

list

tuple

types

function

对于Haskell的纯函数而言,其主要书写格式就是大名鼎鼎的模式匹配,Rust也学习了这套语法。模式匹配其实是将数学按照构造函数进行拆分,能够这么干的主要原因是Haskell是纯函数式语言,没有副作用,变量被赋值后不能够再次修改。
- 最好自己写上函数的签名,方便在编译期查错
- 对于函数来说,从上到下匹配,允许递归
- 对于switch...case类型的句子,使用Guard(|),如果条件需要计算,在后面使用where(允许多个变量,但是要垂直隔开)
- 也可以使用let..in表示中某个作用域中的变量声明
- let是一个表达式,而where是一个语法结构,表达式(类似if..then..else)可以放在各种位置
- 除了这些以外,case表达式本身也存在,其语法结构是case xx of x -> ...,模式匹配本质上都是这个表达式的语法糖
- 使用_可以匹配任意情况
- 除了纯函数外,Haskell也有有副作用的函数比如IO,这种函数的书写格式更类似普通命令式语言。
- 函数的基本形式是a->b->c,箭头连接各个参数,由于函数是柯里化的,所以也可以看做a->(b->c),每个部分是一个偏函数,整个被称为全函数。如果函数不返回任何东西(在某些语言中被称为过程),这个函数肯定是非纯函数,一般返回()【读作unit】,如IO ()

lambda

lambda表达式本来就源自FP(lisp),所以很自然的:\xs -> length xs,不过haskell中用lambda比其他语言应该少很多。

常用的一些函数:
fold, fold1, foldr, scan家族类似
$ 符号同样也可以调用函数,但是与空格不同,他具有最低优先级,主要用来减少括号的数量;
.符号表示右结合的函数,即先算最右边的,然后依次应用左边的函数,右边函数的返回值是左边函数的参数;
上面两个符号主要用来做函数组合,简化书写。。

模块

熟悉的import语句,不过和python的语法不太像,倒是有点像java.

自定义结构

关键字: data
和其他语言不一样,标准格式很奇怪

  1. data BookInfo = Book Int String [String]
  2. deriving (Show)

BookInfo就是类型的名字(类型构造器),Book是值构造器的名字,后面是成员;两者名字可以一致;

这样访问属性还要写专门的函数(模式匹配),很蛋疼,所以有个惯用法(标准称呼是记录):

  1. data BookInfo = BookInfo {
  2. price :: Int,
  3. author :: String,
  4. buyer :: [String]
  5. } deriving (show)

这些成员当然也可以是函数。

递归定义也是很常见的,比如一个二叉树:

  1. data Tree a = Node a (Node a) (Node a)

这里a是类型参数。

关键字type类似C++中的typedef,用于定义类型的别名。

此外newtype也可以自定结构,不过有很多约束。newtype关键字给现有类型一个不同的身份,因此只能有一个值构造器,并且这个构造器只能有一个字段。

data关键字创建的类型在运行时有一个记录开销,如记录某个值是用哪个构造器创建的。而newtype只有一个构造器,所以不需要这个额外开销。这使得它在运行时更省时间和空间。由newtype的构造器只在编译时使用,运行时甚至不存在。

typeclass

就是其他语言中的类型类/接口/模板,语法是

  1. class BasicEq a where
  2. isEqual:: a -> a -> Bool

实例是

  1. instance BasicEq bool where
  2. isEqual True True = True
  3. isEqual False False = True
  4. isEqual _ _ = False

错误

使用 error 函数输出错误
避免抛出错误,使用Maybe, JustNothing

缩进

Haskell 依据缩进来解析代码块。这种用排版来表达逻辑结构的方式通常被称作缩进规则。在源码文件开始的那一行,首个顶级声明或者定义可以从该行的任意一列开始,Haskell 编译器或解释器将记住这个缩进级别,并且随后出现的所有顶级声明也必须使用相同的缩进。
let 表达式和 where 从句的规则与此类似。一旦 Haskell 编译器或解释器遇到一个 let 或 where 关键字,就会记住接下来第一个标记(token)的缩进位置。然后如果紧跟着的行是空白行或向右缩进更深,则被视作是前一行的延续。而如果其缩进和前一行相同,则被视作是同一区块内的新的一行。

也可以使用显式语法结构(使用花括弧代替缩进),不过一般不这么做

节其实是高阶函数的一种形式(语法糖),如

  1. test = (+2)
  2. // test 3 = 5

As模式

模式 xs@(_:xs') 被称为 as-模式,它的意思是:如果输入值能匹配 @ 符号右边的模式(这里是 (_:xs') ),那么就将这个值绑定到 @ 符号左边的变量中(这里是 xs )。

除了增强可读性外,可以简化代码,减少内存分配

折叠

一种思考 foldr 的方式是,将它看成是对输入列表的一种转换(transform):它的第一个参数决定了该怎么处理列表的 head 和 tail 部分;而它的第二个参数则决定了,当遇到空列表时,该用什么值来代替这个空列表。

module

module的规定类似java,名字必须和文件名一致,首字母必须大写;

  1. module SimpleJSON(
  2. Xxxx, //导出部分,如果省略则全部导出
  3. ) where

显然入口模块应该是Main.hs

build

单文件可以用runghc命令运行,相当于脚本;也可以在ghci里面:l外部文件进行测试;
编译则需要使用stack ghc命令;

IO

技巧

数据结构

除了ListTuple, Haskell自带了其他的一些常见的数据结构;

字典

通用序列

Data.Sequence提供了比默认list更好的效率
- 使用fromList创建,或者用emptysingleton函数创建
- 使用|>, <|>< 添加元素,箭头指向被添加的元素
- 使用Foldable.toListSequence转回list

Functor(函子)

对于一个递归的数据结构,对于应用于其上的函数也很可能是同构递归的。书中的例子是将一棵字符串树转变为包含字符串长度的树,也就是说,树的结构不变,但是元素变成长度:

  1. treeLengths:: Tree -> Tree
  2. treeLengths (Leaf s) = Leaf (treeLengths s)
  3. treeLengths (Node l r) = Node (treeLengths l) (treeLengths r)

这种能够同构映射的,满足类型类Functor,映射函数即fmap

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

Functor是非常重要的一类class,有几个基本原则进行约束。首先,fmap id必须返回相同的值,其次,Functor必须是可组合的,换句话说,fmap a . fmap b == fmap (a . b)应该成立。

编译器不会检查这些规则,程序员需要自己保证这些规则成立。这些规则是函子在范畴论中的数学约束。

幺半群

在范畴论中,有一类简单的抽象结构被称为幺半群。许多数学结构都是幺半群,因为成为幺半群的要求非常低。 一个结构只要满足两个性质便可称为幺半群:

即:

  1. class Monoid a where
  2. mempty :: a -- the identity
  3. mappend :: a -> a -> a -- associative binary operator

如果我们真的需要在同一个类型上实现多个 Monoid 实例,我们可以用 newtype 创建不同的类型来达到目的。

Monads

Monad(单子)是Haskell最难理解的东西之一,这个概念和上文中的幺半群有关,这里有一些辅助理解的漫画。

一个Monad由以下几个构造元素:

标准的monad定义为:

  1. class Monad m where
  2. -- chain
  3. (>>=) :: m a -> (a -> m b) -> m b
  4. -- inject
  5. return :: a -> m a

显然>>=就是串联函数,return是注入函数(IO monad就是把普通值放入IO),类型构造器就是m本身。除此之外,还有>>函数用户忽略返回值的过程(即按步骤一步步来),以及fail函数接受一个错误消息让整个调用链失败(默认使用error函数)。

下面是具体的解析:


  1. liftM :: (Monad m) => (a -> b) -> m a -> m b
  2. liftM f m = m >>= \i ->
  3. return (f i)

该函数被定义在Control.Monad中。比如我们要计算Just (1 + 3),就可以用

  1. -- Just 4
  2. ( 1 + ) `liftM` (Just 3)

这与 Just 3 >>= \x -> Just (x+1)等价。

另外,从函子的角度,这个也可以写作(1+) <$> (Just 3)

如果函数f有多余一个参数,Control.Monad中也有对应的liftM2~liftM5

  1. fee :: Int -> Int -> Int
  2. fee a b = a + b

那么 liftM fee的类型为:

  1. Monad m => m Int -> m (Int -> Int)

显然这个函数的返回值满足ap的参数需求, 所以以下两个表达式相等:

  1. fee `liftM` (Just 1) `ap` (Just 1)
  2. fee `liftM` (Just 1) <*> (Just 1)
  3. liftM2 fee (Just 1) (Just 1)
  1. join :: Monad m => m (m a) -> m a
  2. join x = x >>= id

用来移除一层Monad,join (Just (Just a))就是Just a

第三条是结合律,如果我们把一个大的action分解成一系列的子action,只要我们保证子action的顺序,把哪些子action提取出来组合成一个新action对求值结果是没有影响的。

  1. class Monad m => MonadPlus m where
  2. mzero :: m a
  3. mplus :: m a -> m a -> m a
  4. instance MonadPlus [] where
  5. mzero = []
  6. mplus = (++)
  7. instance MonadPlus Maybe where
  8. mzero = Nothing
  9. Nothing `mplus` ys = ys
  10. xs `mplus` _ = xs
  1. guard :: (MonadPlus m) => Bool -> m()
  2. guard True = return ()
  3. guard False = mzero

Monad变换器

Monad变换器主要用来修改已经存在的Monad,以T结尾。大量Monad变换器进行叠加,就可以得到拥有多种功能的Monad.

错误处理

讨论一些常用的haskell处理方式:Maybe, EitherException,一般都使用Monad配合。

再往后就是一些实际使用的例子了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注