zoukankan      html  css  js  c++  java
  • Haskell学习笔记<四>

    《learn you a Haskell》这书的结构与常见的语言入门教材完全不一样。事实上,即使学到第八章,你还是写不出正常的程序…因为到现在为止还没告诉你入口点模块怎么写,IO部分也留在了最后几章才介绍。最重要的是,没有系统的总结数据类型、操作符、语句,这些知识被零散的介绍在1-8章的例子中,换句话来说,这书其实不算是很合格的教材(代码大全那种结构才更适合),不过它重点强调了FP与其他语言的思想差异,对于已经有其他语言基础的人来说,读这本书能够很快领悟到FP的妙处,对于那些并不用它来做项目(估计也很难有公司用这个做项目,太难了)的人来说,这本书很适合作为拓展阅读的小册子。另外一本《Real World Haskell》也相当有名,我还没有看,这本看完后再着手阅读吧。

    第八章 自定义类型和类型类

    关键字data用来构建类型,格式为

    data classname = definition

    左端是类型的名字,右端是该类型的取值,又称为构造子。例如:

    data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

    这里表示类型Shape可以取值为Circle或Rectangle,这两个类型的构造子分别是3个、4个Float。构造子本质是一种函数,取几个参数返回一种类型。比如这里的Circle :: Float->Float->Float->Shape。关键字deriving表示派生于某类型类,即具有该类型类的性质。这里面有一些类似于面向对象中的封装与继承的概念,比如可以认为Shape是基类,Circle和Rectangle是派生类。如果某个函数对Shape通用,那么在模式匹配中就将两种派生类的实现都写出来,否则只匹配部分类即可。注意Circle和Rectangle并不是自定义类型,并不能直接使用,只有在需要Shape时,用构造子进行匹配来使用。如果构造子只有一个,可以让data类型与构造子同名,这样更加清晰。

    如果说第七章的module相当于C++中的namespace,那么本章的data就相当于class,不同的是module封装的是function,而Haskell中class就是function,所以module、data和function就构成了Haskell中的用户层次。

    导出格式:

    module Shape

    (Shape(…)

    ,fun1

    ,fun2

    )

    其中Shape(…)表示导出全部构造子。

    Record Syntax

    我们知道C++中提倡软件工程中的封装概念,尽量不让内置数据类型暴露给使用者,一般要写一大堆setxxx,const getxxx函数,虽然这些小函数往往只有一句话,但是写多了也很烦。Record Syntax是Haskell中一种用来简化此类函数书写的语法结构。在声明data时,直接标明其参数名称和对应的数据类型:

    data Shape=Circle { x::Float,y::Float,radix::Float} | Rectangle { x1::Float,y1::Float,x2::Float,y2::Float} deriving (Show)

    这里的语法有点像C中的位域,但是它实际的意思其实仍然是前面学过的类型解释符。Haskell会自动生成已经注明名称的参数的函数。另外在调用构造子时,也要遵从这种结构,不过x::Float换成x=1.0(即Circle {x=1.0,y=2.2,radix=3.1})。

    类型参数

    前面学习了值构造子,这里介绍类型构造子。值构造子显然需要明确值的类型,而类型构造子则宽松的多,比如向量,我们只需要向量的参数类型一致即可,不必明确具体的类型。换句话说,类型参数是一种"泛型"语法支持(类似C++的模板)。

    比如 data Vector a = Vector a a a deriving (Show),这就是一个三维向量。类型参数a对后面的值构造子产生约束。也就是C++中的:

    template <class a>

    class Vector

    {

        Vector(a first,a second, a last);

    }

    显然我们在写函数的时候需要对类型参数进行实例化,换句话说,需要对函数进行类型约束。记住,不要在data声明中添加类型约束,而是在函数中添加,因为具体执行操作的是函数,而数据类型需要比函数更加抽象。这里按着C++中写模板的思路来就很容易理解什么时候用类型构造子。

    派生实例

    本节介绍了从类型类从派生类型的方法。前面已经介绍过类型类本质上是一种接口要求,它描述了类型的行为一致性。在我们创建类型时,可以在值构造子后面加上deriving (Ord,Show,Read,…),来给创建的类添加接口。一旦加上了指定的类型类接口,就给予所创建的类对应的行为特性。如果加了Ord,就可以直接比较两个类(根据值构造子和参数),如果加了Show,那么就可以显示该类的参数(如果使用了syntax record,就显示出"名称=值"的格式。)

    派生实例是很有用的特性,在我们设计类的时候需要明确该类支持的行为特性。这看起来有些像C++中的重载操作符,但是不需要我们自己去实现。Haskell会自动推断应该怎样实现声明的行为。

    注意Haskell中True/False与C中意义完全不同【C语言中没有bool,只是单纯认为0为false,非0为true】,可以认为 data Bool = False | True deriving (Ord),所以True > False是成立的。同理Nothing总是小于Just a。

    类型别名

    类似typedef的语法,在Haskell中格式为

    type Newtype=OldType,与typedef相似的是一般用来简化书写或者明晰概念。与typedef不同的是,type也支持Haskell的不全调用。

    这里举了Either a b作为例子,Either a b是用来代替Maybe a的,当函数的返回类型可能有多种,或者函数需要根据情况返回不同的信息时,经常使用Either a b作为返回类型。

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

    Either a b有两个值构造子,如果用了Left构造子,其返回类型就是a,否则是b,换句话说,Haskell可以返回多种类型的结果,而不像C++那样只能用结构来封装。

    递归数据结构

    【我本以为第8章到这就完了呢…结果后面又发现是翻译的大哥翻到一半就终止了,第八章后面还有不少,这部分是后来添补的】

    考虑list,如果我们需要自己定义list类型,应该如何声明?应该这样:

    data List a = Empty | Cons a (List a) deriving (Show,Read,Eq,Ord)

    这里Cons相当于运算符 " : ",显然这里的List定义是递归的——等号的左右两端都存在List。这样,我们就可以像使用:一样使用Cons来构造List,如 3 `Cons` 4 `Cons` Empty。

    也可以自定义操作符,使用infixr来确定操作符的优先级和左、右结合性,注意这里可以定义任意操作符,这一点和C++中的重载操作符有本质的不同。书中以操作符 :-: 为例介绍了使用方法。

    下面介绍了一个二叉搜索树的生成作为例子:

    data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

    一个二叉树是一个空树,或者一个由根节点和其左子树与右子树构成的树。下面是一些常用操作函数的实现,这里从略。

    TypeClass102

    本节介绍如何自定义类型类,类型类与常见的过程式或者面向对象模型的编程语言并无相似之处,不过可以和C++中的重载操作符对比参考。

    我们使用关键字class来定义类型类,使用关键字instance来定义类型类的实例。

    class Eq a where

    (==) :: a->a->Bool

    (/=) :: a->a->Bool

    x == y = not x /= y

    x /= y = not x==y

    instance Eq TrafficLight where

    instance下面就是详细解释该接口的实现。在不想使用默认的从类型类派生得到的行为时,必须使用instance来自定义数据对于该接口的行为方式。对于Maybe类型的实例,格式是

    instance (Eq m) => Eq (Maybe m) where

    注意这里需要对m添加类型约束。

    可以在GHCI中使用:info 得到类型类、类型和类型构造子的详细信息。

    A yes-no typeclass

    本节讲了一个类型类的实例,它用来完成各种类型向Bool的缺省转换(如C的非零为True,0为False)。

    class YesNo a where

        yesno :: a -> Bool

    实现:

    instance YesNo Int where

        yesno 0 = False

        yesno _ = True

    instance YesNo [a] where

        yesno [] = False

        yesno _ = True

    instance YesNo Bool where

        yesno = id

    …其他略

    注意id是标准库函数。

    函数子类型类

    函数子类型类(Functor class)指的是可以被被映射的函数满足的接口。

    class Functor f where

        fmap :: (a->b) -> f a -> f b

    注意定义中的f并非类型,而是类型构造子。换言之,函数子类型类的实例参数必须是类型类构造子而不能是具体的类型,那么可见这个类型类是容器的接口(如Maybe或[]等拥有1个以上值构造子的类型)。如果f有两个以上的参数,那么只能用函数的不完全调用格式,仅保留一个参数进行处理(如Either a)

    种类和一些类型相关的东西

    本节对类型(type)的知识做了扩展,这里将类型本身分为具体类型和不完全类型。

    这种类型的类型被称为种类(kinds),可以在GHCI中使用:k来对类型进行种类的分析。

    GHCI使用 " * "表示具体的类型,如果不是具体类型,就是可以通过一个或多个参数得到具体类型的不完全类型。

    如果理解了类型构造子本身也是函数这一点,本节的内容还是比较容易理解的。

    第九章 输入与输出

    第九章开始就没有中文的翻译了,只能看英文资料,英文9-14章戳我

    到了第九章,我们终于可以写Hello World了!一本教材讲到大半才讲输入输出的,也算是比较罕见了,呵呵。

    在Haskell中,我们并不能像在命令式编程中一样随意改变非const变量的值。Haskell保持着这样一个特性:对于一个函数,只要它的调用参数不变,那么它返回的值总是不变。Haskell不会试图改变已经确定的变量,而是试图返回新的变量。那么这里出现一个问题:如果Haskell并不改变任何变量,那么它就无法输出——因为输出会改变屏幕。为此,Haskell设计了一种机制将非纯函数式编程(即与输入输出打交道的部分)与函数式编程(即前面八章介绍的内容)隔离开来,这里将这种机制称为side-effects,直译为边界效应。

    Hello World!

    Hello World总是每种语言必须提到的东西。对于Haskell,输出Helloword只需要一行代码,嗯,不愧是优雅与简洁的典范。

    main = putStrLn "Hello world!"

    main表示主函数,所有涉及IO的函数都在main中执行。所以main函数经常被写作main:: IO (),当然()也可以换成其他的返回类型。

    putStrLn函数的解释是

    putStrLn :: String->IO ()

    也就是输入一个字符串,执行一个IO action,返回一个空的tuple。

    因为IO行为是非纯函数行为,所以Haskell设计了do块来将所有的非纯函数进行封装,最后通过<-操作符将IO取得的值绑定到一个变量上。do块中,除了最后一个IO action 其他的均可绑定到一个变量上,不过如putStrLn这种函数的返回值肯定是(),所以绑定没什么意义。最后一个IO action会将返回值绑定给main本身。do块这种行为方式类似于verilog中的begin…end语句块。

    如果不涉及IO,仍然使用前面学过的let … in…来直接绑定变量,不过这里in可以省略,缺省成为整个do块中有效。

    return语句:在haskell中return语句只能在IO块中使用,它表示一个IO行为,输出一个可以通过变量绑定的值。return语句并不能从该段程序中返回。我们只有在需要执行一次什么都不做的IO或者不希望返回最后一个IO action取得的值时使用return语句。

    main:main本身即是一个IO函数,所以可以通过在main函数结尾调用它来递归该函数。

    其他IO函数:

    函数原型

    解释

    putStr

    类似putStrLn,但尾部不输出换行符

    putChar/getChar

    输出/入字符

    print:: Show a => a -> IO ()

    输出一切属于show类型类的数据

    sequence:: Monad m => [m a] -> m [a]

    执行参数1中的I/O动作,返回动作的结果

    mapM :: Monad m => (a -> m b) -> [a] -> m [b]

    map的I/O版,相当于sequence . map

    mapM_ :: Monad m => (a -> m b) -> [a] -> m ()

    同上,只是不再返回I/O动作的执行结果

    注意map一个I/O动作到一个list中,并不会真正执行这个list中的动作。想要真正执行,必须使用sequence函数,当然,方便起见,可以使用mapM或mapM_。

    这一块介绍了Control.Monad内的几个函数,when函数取一个布尔值和一个I/O动作作为参数,如果bool值为真,执行该动作,否则返回一个什么都不执行的I/O动作;forever永久执行参数中的I/O动作;forM类似于mapM,只是参数的顺序颠倒。

    文件与流

    其实I/O这一块Haskell与命令式语言并无不同,要注意的只是do块的位置和I/O函数结果的绑定。对于文件I/O,haskell与C基本一致,常用函数如下:

    函数原型

    解释

    getContents :: IO String

    从标准流读入数据直到EOF(Ctrl+D)

    interact :: (String -> String) -> IO ()

    对输入执行参数1的函数,输出结果

    openFile :: FilePath -> IOMode -> IO Handle

    System.IO,打开文件,选择方式,返回句柄

    hGetContents :: Handle -> IO String

    根据句柄返回文件内容

    hClose :: Handle -> IO ()

    根据句柄关闭文件

    withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r

    整合的一个函数,打开文件并使用参数3处理文件,完成后关闭文件

    hGetLine/hPutStr/hputStrLn/hGetChar

    大体同标准流,只是参数中多了文件句柄

    readFile :: FilePath -> IO String

    读取文件

    writeFile :: FilePath -> String -> IO ()

    写入文件,模式为截断

    appendFile :: FilePath -> String -> IO ()

    写入文件,模式为追加

    getTemporaryDirectory :: IO FilePath

    System.Directory,读取临时文件夹路径

    openTempFile :: FilePath -> String -> IO (FilePath, Handle)

    System.IO,打开临时文件,返回一个tuple

    removeFile/renameFile

    System.Directory,删除、重命名文件,参数是路径不是句柄

    注意这里的读取文件相关函数都是惰性的。以上青色字体来自System.IO库,没有注明的来自Precluded库。使用句柄做参数的函数均以h开头。

    缓冲控制:如果需要修改编译器默认的缓冲机制,可以使用函数hSetBuffering来修改,使用hFlush来强制刷新缓冲区

    这里介绍了Unix下管道操作符 | 的使用。简单来说,可以通过管道操作符将上一个动作的输出作为下一个动作的输入。

    命令行参数

    同C语言程序一样,Haskell也是可以接受命令行参数的。C语言将命令行参数作为main函数的参数传递,而Haskell主要使用两个函数来取得用户输入的参数,import System.Environment

    getArgs :: IO [String]

    取出所有参数

    getProgName :: IO String

    取得程序名称

    另外后面给了错误退出的函数(类似<stdlib>中的exit函数):errorExit。

    随机数发生器

    随机数发生器在任何语言中都是标准库自带的函数/类。虽然Haskell要求纯函数的输入一定时,输出固定,但是实际上几乎所有的语言中随机数发生器生成的都是伪随机数,所以Haskell这个特性并不意味着其实现比一般的语言困难。相关函数如下(import System.Random):

    random :: (RandomGen g, Random a) => g -> (a, g)

    参数给出一个随机数种子,返回一个随机数和一个新的种子

    mkStdGen :: Int -> StdGen

    以一个整数为参数,生成一个标准的种子

    randoms :: (RandomGen g, Random a) => g -> [a]

    根据种子生成一个无限长的随机序列

    randomR :: (RandomGen g, Random a) => (a, a) -> g -> (a, g)

    参数1的pair限制了最后取得随机数的范围

    randomRs :: (RandomGen g, Random a) => (a, a) -> g -> [a]

    根据参数1生成规定范围的无限长list

    getStdGen :: IO StdGen

    取得一个全局的随机数发生器种子

    newStdGen :: IO StdGen

    刷新全局随机数发生器

      

    注意random函数的返回类型可以是任何类型,所以在使用的时候必须在后面加上类型约束::(type1,type2)作为随机数和种子的类型,如果使用StdGen的种子,则一般返回类型为(Int,StdGen)。

    如果不执行newStdGen,那么getStdGen总是返回同样的种子。

    另外这里还介绍了read加入了错误处理的版本reads,后者再不能读取参数时将返回空list。

    二进制字符串

    不清楚这个翻译是否合适,总之Bytestrings主要介绍二进制读写文件的方法。不同于C语言,这里有两个版本的二进制读取,一个是严格的非惰性版本,另外一个是惰性版本,分别来自Data.ByteString和Data.ByteString.Lazy. 对于lazy版本,这里和前面介绍的文件IO函数的实现也有所不同——它每次最少读取64K字节的东西。大体来讲ByteString的相关函数与Data.List中的函数接口一致,不过在类型约束中用ByteString代替了[a],用Word8代替了a。函数pack将Word8打包成ByteString,unpack用于解包;fromChunks将严格版(非惰性)转换为惰性版,toChunks则相反;cons和cons'用来取代list的 :操作符,注意后者适用于非惰性版;还有其他一些函数,对应于list中的某些函数,这里就不列举了。

  • 相关阅读:
    spring揭密学习笔记(3)-spring ioc容器:Spring的IoC容器之BeanFactory
    spring揭密学习笔记(3)-spring ioc容器:掌管大局的IoC Service Provider
    spring揭密学习笔记(2)-spring ioc容器:IOC的基本概念
    spring揭密学习笔记(1) --spring的由来
    spring揭密学习笔记
    spring事务管理实现原理-源码-传播属性
    spring事务传播实现源码分析
    IDEA搭建Spring框架环境
    ScrollView滑动的监听
    Android对apk源代码的改动--反编译+源代码改动+又一次打包+签名【附HelloWorld的改动实例】
  • 原文地址:https://www.cnblogs.com/livewithnorest/p/2620718.html
Copyright © 2011-2022 走看看