• Haskell学习笔记一:类型和类型类相关内容


    内容提要:

    静态类型系统;

    编译时确定类型错误;

    类型推导机制;

    基础类型:Int,Integer,Float,Double,Bool,Char;

    类型变量;

    基础类型类:Eq,Ord,Show,Read,Enum,Bounded,Num,Integral,Floating;

     

    Haskell是一门函数式编程语言,被称为最为纯粹的函数式编程语言。Haskell的类型系统非常强大,其中包含了很多有趣、抽象、某种程度上充满学术气息的特质。

     

    Haskell属于静态类型语言,这意味着:

    • 每个值或者表达式,都具备特定的类型,无论这个类型是系统默认的,还是用户自定义的;
    • 在编译时,能够检查类型错误;  

     

    以上两个特征没有什么特别,很多现代编程语言,也是静态类型的,比如Java,C#等,都具备这些特征。但是Haskell的类型系统,还有一个强大特征:能够根据值或

    者表达式进行类型推导,这意味着在Haskell中,绝大多数情况下,可以不显示指定值或者表达式的类型,用类似动态语言的简洁方式编写代码,同时享受静态语言的

    编译时检测类型安全机制。

     

    可以在GHCI(一个Haskell的交互式编程环境,参考https://www.haskell.org/platform/)中,通过命令“:t”后面跟值或者表达式来显示查看对应的类型,比如:

    -- 两个小横线开头的行,表示是注释
    :t 'a'
    -- 显示结果为:'a' :: Char
    :t (1+1)
    -- 显示结果为:(1+1) :: Num a => a
    :t True
    -- 显示结果为:True :: Bool

    可以这样理解,'a'的类型为Char;True的类型为Bool;表达式 (1+1) 的类型为符合类型为Num类型类实例的任何类型,其中小写字母a代表类型变量,这种表示方式

    是Haskell特有的,后面再详细学习介绍。

     

    基础类型

    先了解几个Haskell自带的基础类型,和其他编程语言中对应的类型很类似:

    Int

    表示整型数字,但是有最大值和最小值,可以通过如下的方式查询Int在机器上的最大值和最小值:

    maxBound :: Int  
    -- 我自己的机器上,结果为:9223372036854775807
    
    minBound :: Int
    -- 结果为:-922337203685477808
    
    -- maxBound 和 minBound 函数是多态的,能够运用到实现了Bounded类型类实例的任何类型,但是具体调用的时候,需要显示指明其返回值类型,比如这里的Int。

    Integer

    表示整型数字,但是没有限制最大值和最小值,可以表示极大或者极小的整数,但是相对于Int,效率上要低些,比如下面定义了一个阶乘函数,然后调用其计算50

    的阶乘:

    -- 阶乘函数定义,第一行显示指明函数的参数和返回值类型
    -- 第二行是阶乘的实现,将product函数应用到有1到n组成的列表中即可 
    factorial :: Integer -> Integer
    factorial n = product [1..n]
    
    -- 在GHCI中调用factorial函数:
    factorial 50 
    -- 显示结果:30414093201713378043612608166064768844377641568960512000000000000

    Float和Double

    Float表示单精度的浮点数类型,Double表示双精度的浮点数类型,通过下面的一个例子来展示两者的区别:

    -- 定义一个根据半径计算圆周长的函数,参数和返回值类型都是Float
    circumference:: Float -> Float
    circumference r = 2 * pi * r
    
    -- 在GHCI中执行
    circumference 4.0
    -- 结果为:25.132742
    
    -- 定义一个根据半径计算周长的函数,参数和返回值类型都是Double
    circumference' :: Double -> Double
    circumference' r = 2 * pi * r
    
    -- 在GHCI中执行
    circumference' 4.0
    -- 结果为:25.132741228718345

    Bool

    Bool类型只有两个值,True和False,表示逻辑真和假,使用方式和其他语言类似:

    -- 以下代码直接在GHCI中执行
    :t True
    -- 结果为:True :: Bool
    
    :t False
    -- 结果为:False :: Bool
    
    (1+1) == 2
    -- 结果为:True
    
    True && False
    -- 结果为:False
    
    True || False
    -- 结果为:True
    
    if True then "is true" else "is false"
    -- 结果为:"is true" 

    Char

    Char表示字符类型,使用单引号包含;单字符的用途有限,字符串的使用场景更多,在Haskell中,字符串使用双引号包含,字符串只是字符列表的语法糖,如下:

    -- 单字符即Char类型
    :t 'C'
    -- 结果:'C' :: Char
    
    -- 字符串只不过是字符列表而已
    :t "Seaman"
    -- 结果:"Seaman" :: [Char]

    类型变量

    为了方便说明类型变量在Haskell中的作用,先简单说明Haskell中的函数定义。因为Haskell本身是一门函数式编程语言,所以程序内容都是在定义函数,使用函数。

    典型的函数定义方式有两种:

    1、只有函数体,不显示说明参数和返回值类型,通过Haskell的类型推导机制确定参数和返回值类型,比如:

    -- 直接定义只有函数实现的一个加法函数
    sumF a b = a + b
    
    -- 在GHCI中调用
    sumF 2 3
    -- 结果:3
    sumF 2.0 3.0
    -- 结果:5.0

    2、显式指定函数参数和返回值类型,比如:

    -- 定义只能支持Int类型的加法函数
    -- 第一行显式声明sumF'函数接受两个Int类型的参数,并且返回一个Int类型结果
    sumF' :: Int -> Int -> Int
    sumF' a b = a + b
    
    -- 在GHCI中调用
    sumF' 2 3
    -- 结果:5
    sumF' 2.0 3.0
    -- 结果:由于函数不支持Float的参数,所以报错

    很有意思的是,没有显式定义参数和返回值类型的sumF,居然自己支持多态,可以传入两个Int类型的参数,返回一个Int类型的结果;或者传入两个Float类型

    的参数,返回一个Float类型的结果,可以看看Haskell为其指定的默认函数规格说明:

    -- 查看sumF的函数规格说明
    :t sumF
    -- 结果:sumF :: Num a => a -> a -> a
    -- sumF函数接受两个参数,返回一个结果
    -- 其中两个参数和结果的类型一致
    -- 它们的类型必须都是Num类型累的实例

    抛开略显得古怪的函数规格说明方式(后面的学习会详细解释),我们可以看到,结果中的小写字母a就是类型变量,它代表一种抽象类型,而非具体的类型(比如Int、

    Float或者Bool等等,都是具体的类型)。它表示任何Num类型类的实例(具体的类型,比如Int、Float等都是Num的实例)都可以作为参数调用该函数,并且返回值的类型

    和参数是一样的。

    Haskell中经常使用小写字母,比如a,f,m等代表类型变量,并且形成了一套类型体系机制,英文名称是:Algebraic Data Types,翻译过来就是:代数数据

    类型。仔细体会一下,确实有很浓厚的数学意味,而且至少我能理解到有如下两个突出的优点:

    1、 抽象,有些组合数据的类型操作,是无关于其中具体类型的,比如列表的一些操作(列表的长度、遍历列表等),树的一些操作(树的节点数、树的深度等),

      都是和其中具体类型无关的,类似[a], Tree a都是很好的抽象;

    2、 抽象,直接支持多态的函数,比如之前的加法函数,有比如Map,Reduce之类基于多个具体数据之上的高阶函数。

    Haskell的类型变量很类似于C#中的泛型,并且结合Haskell特有的类型类使用,可以形成高度抽象,完全基于行为的鸭子类型,下节就具体介绍类型类。

      

    类型类

     

    在上面的例子中,Num就是一个类型类。和面向对象语言(比如C#,Java)中的类不同,Haskell的类型类,是关于行为的,即类型类是一种接口,其中定义了必须实现的

    行为(方法)。

    类型类的主要作用是定义接口行为(方法),并且对具体的类型,或者其他类型类进行约束。如果一个类型类对一个具体类型进行了约束,那么这个具体类型的定义,就

    必须包括类型类的行为(方法)实现。这样的类型约束方式,或者说类型系统组成方式,都是基于行为的,是完全鸭子类型的。比如Haskell中的一个特别基础Eq类型类,

    定义的行为就是关于如何比较两个类型相同的值,几乎所有Haskell的具体类型,都是Eq类型类的实例,实现了基于自己类型的相等和不等方式。

    下面具体介绍在Haskell中最为基础的一些类型类:

    Eq

    Eq类型类用于支持相等性测试。其中定义了两个方式:==和=,分别用于判断相等和不相等,可以在GHCI中使用:info命令,查看类型类的详细信息,如下:

    :info Eq
    
    -- 显示结果如下:
    class Eq a where
        (==) :: a -> a -> Bool
        (=) :: a -> a -> Bool
    
    -- 第一行是类型类的定义方式,关键字class;a是类型变量,用于抽象类型
    -- where后两行,表示Eq类型类定义的两个行为(方法)
    -- 如果方法名全部是特殊字符组成,那么方法也称为操作符
    -- 操作符使用小括号包含,天然支持中缀表达式

    Haskell中的基础类型,都是实现了Eq类型类的具体类型,例如:

    5 == 5
    -- 结果:True
    5 /= 5
    -- 结果:False
    'a' == 'a'
    -- 结果:True
    “Seaman” == “Seaman”
    -- 结果:True

    同时,由于(==)和(=)本质上都是函数,可以通过:t来参看其函数签名:

    :t (==)
    -- 结果为:
    (==) :: (Eq a) => a -> a -> Bool
    -- 可以这样解读这个函数签名:输入两个相同类型的参数,返回一个Bool值
    -- 其中输入参数是类型类Eq的具体实现类型,即a支持Eq中定义的方法

    这里可以看到Haskell中类型变量表达方式的强大,它抽象了类型的表达方式,使用类似数学中代数的概念,使得概念清晰明确,极具表达性。

    类型类定义起行为的时候,可以同时定义实现,比如一些和具体类型无关的实现,可以是支持该类型类的具体类型都一致的;同时如果具体类型的实现是基于自身特殊的,就

    需要在定义具体类型的同时,实现其行为方法(后续学习章节详细介绍)。

    Ord

    Ord类型类定义了比较和顺序相关的行为(方法),具体的需要实现的方法如下:

    :info Ord
    -- 结果如下:
    class Eq a => Ord a where
        compare :: a -> a -> Ordering
        (<) :: a -> a -> Bool
        (>=) :: a -> a -> Bool
        (>) :: a -> a -> Bool
        (<=) :: a -> a -> Bool
        max :: a -> a -> a
        min :: a -> a -> a
    
    -- 注:Ord的定义中,包括了类型限制,即必须满足Eq类型类的行为

    Ord类型类支持的行为包括:compare,<,>=,>,<=,max,min,根据函数的名字和签名,可以很明确的知道其代表的含义。唯一需要说明一下的是,compare的返回值

    是一个Ordering类型,这是一个具体的类型,包括了三个值:GT,LT和EQ,分别表示大于,小于和等于。一些关于实现了Ord类型类的具体类型事例如下:

    "ABCD" < "EFGH"
    -- 结果为:True
    "ABCD" `compare` "EFGH"
    -- 结果为:LT
    5 >= 2
    -- 结果为:True
    5 ·compare· 3
    -- 结果为:GT

    可以从上面的例子中看到,Haskell的基础类型,比如Int,Char,String等,都是Ord类型类的具体实现类型。

    Show

    Show类型类定义了如何使用字符串显示类型实例的方法,类似于C#的Object基类中的ToString()方法干的事情,比如:

    -- show方法是Show类型类中定义的
    show 3
    -- 结果为:"3"
    show 5.23
    -- 结果为:"5.24"
    show True
    -- 结果为:"True"

    Read

    Read类型类定义了如何从输入终端读入字符串,并且转型为具体类型,可以理解是和Show类型类完成相反的操作,比如:

    -- read方法是Read类型类中定义的
    -- 可以通过Haskell的类型推导隐式转型使用
    read "True" || False
    -- 结果为:True
    read "5" + 5
    -- 结果为:10
    read "8.2" + 1.8 
    -- 结果为:10
    
    -- 也可以进行显式地类型说明,直接读入为指定类型
    read "10" :: Int
    -- 结果为:10

    Haskell的基础类型(比如Int,Char,Float,Double等)都是Show和Read的具体实现类型。

    Enum

    Enum类型类定义了可以枚举的有序类型应该支持的行为,Haskell的一些基础类型,比如Bool,Char,Ordering,Int,Interger,Float,Double都是Enum类型类的

    具体实现类型。Enum类型类中,最为重要的应用是Range和succ、pred方法,比如:

    ['a' .. 'e']
    -- Range语法,两点表示从字符a到e的有序列表
    -- 结果为:"abcde"
    
    [LT .. GT]
    -- 结果为:[LT, EQ, GT]
    [3 .. 5]
    -- 结果为:[3,4,5]
    
    succ 'B'
    -- 字符B的下一个字母,结果为:'C'
    pred 'B'
    -- 字符B的上一个字母,结果为:'A'

    Bounded

    Bounded类型类定义了支持最大值和最小值的行为,比如Int、Bool都是其具体实现类型,比如:

    -- Bounded的两个方法是:minBound和maxBound
    minBound :: Int
    -- 结果为:-9223372036854775808
    maxBound :: Char
    -- 结果为:'1114111'
    maxBound :: Bool
    -- 结果为:True
    minBound :: Bool
    -- 结果为:False

    Num、Integral和Floating

    在Haskell的类型体系中,Num是关于数字的类型类,其中提供了所有数字(实数、虚数、有理数、整数、小数)都支持的行为,比如+、-、*、negate、abs等;

    Integral是Num的一个具体实现,同时也是所有整数的类型类,提供了诸如rem、div、mod等基于整数的行为;

    Floating是Num的另外一个具体实现,同时也是所有浮点数的类型类,提供了诸如pi、exp、sqrt、log等基于浮点数的行为。

    如果需要将Integral和Floating类型的数值放在一起计算,需要显式地进行转型,一个常用的方式是:fromIntegral,这个方法的签名如下:

    fromIntegral :: (Num b, Integral a) => a -> b

    即把Integral类型的变量a,转型为更为一般形式的Num类型,然后就可以和其他Floating类型进行运算了。

    总结:Haskell本质上是静态强类型的语言,其中静态类型特征保证了编译时能够发现任何类型方面的错误;同时类型推导机制,又十分方便编写基于问题本身的代码,而

    非各种类型说明和定义。而这一切的基础,和Haskell的代数数据类型机制是密不可分的,其中的关键是类型变量和类型类。类型变量使用小写字母表示抽象而非具体的类

    型,在多态语义的上下文环境中,十分具有表达力。类型类基于行为定义去约束具体的类型,从而从根本上构建出Haskell纯粹的鸭子类型。这一切结合在一起,使Haskell

    的类型系统很令人着迷,我们继续学习吧!

    本文部分内容和实例来自下面的网址:http://learnyouahaskell.com/types-and-typeclasses#typeclasses-101

     

  • 相关阅读:
    HDU 3123-GCC(递推)
    新交互英语外挂全自己主动版
    BZOJ 2716 Violet 3 天使玩偶 CDQ分治
    关于 FPGA 和 外部芯片接口时序设计
    Ubuntu启动、停止、重新启动MySQL,查看MySQL错误日志、中文编码错误
    Drupal 7 建站学习手记(四):怎样改动Nivo Slider模块的宽高
    Linux下安装Oracle的过程和涉及的知识点-系列4
    游戏开场镜头拉近(Unity3D开发之四)
    并发编程
    给线程发送消息让它执行不同的处理
  • 原文地址:https://www.cnblogs.com/seaman-h-zhang/p/4553782.html
Copyright © 2020-2023  润新知