Skip to content

Latest commit

 

History

History
269 lines (172 loc) · 6.87 KB

05 泛型函数和类型类.md

File metadata and controls

269 lines (172 loc) · 6.87 KB

泛型函数和类型类

泛型类型

data A2 a = G2 a

这是说, 定义一个类型叫A2, 它有一个泛型参数a.

所谓泛型, 就是在定义时不确定的东西, 在实际使用时才会确定.

这里说a是不确定的, 同时说, 构造子G2需要传入一个a类型的参数.

接下来就可以调用:

x2_1 :: A2 Int
x2_1 = G2 1

x2_2 :: A2 String
x2_2 = G2 "aaa"

注意到, 通过指定类型, 即可确定泛型的实际值.

x2_1中, a被确定为了Int. 所以调用G2的参数也变成了Int类型的值.

x2_2中, a被确定为了String. 所以调用G2的参数也变成了String类型的值.

需要注意的是, A2 Int这个整体是一个类型. 同时, A2也算是一个类型, 但他们也不太一样. 我们可以规定类型的类型, 称为Kind类型(种类). 像A2 Int这样的类型, 它的种类是Type. 而A2这样的泛型类型, 它的种类是Type → Type. 关于种类的问题很复杂, 这里我们不讨论. 但有一个规定, 若有一个值a, a的类型是t, 那么t的种类一定是Type.

多泛型类型

泛型可以有多个:

data A3 a b = G3 a b String

x3 :: A3 Int String
x3 = G3 1 "aaa" "bbb"

函数泛型

泛型也可以用在函数里

f5 :: forall a. a -> Int
f5 x = 1

这表示, a是一个泛型, 这个函数接受一个a类型的值, 返回一个Int值.

所以a会在调用时被确定.

类型类约束

不幸的是, 如果仅仅有一个不确定类型的值, 那么我们对它什么都做不了.

例如:

f7 :: forall a. a -> String
f7 x = x <> "_"

这是说, 输入一个a类型的值, 返回一个字符串.

我希望把a类型的值和另一个字符串拼接在一起.

但问题是, 只有两个字符串才可以拼接在一起, 这个x是类型是不确定的, 所以不能这样写.

这时候就可以使用类型类:

class CanToString a where
  toStr :: a -> String

这定义了一个类型类, 并规定, 任何实现此类型类的类型a, 都有toStr函数.

toStr函数可以把a转换为字符串.

现在我们实现几个类型:

instance CanToString Int where
  toStr :: Int -> String
  toStr a = show a

instance CanToString Boolean where
  toStr :: Boolean -> String
  toStr true = "T"
  toStr false = "F"

这样, 就可以通过类型类约束泛型了:

f7 :: forall a. CanToString a => a -> String
f7 x = (toStr x) <> "_"

这是说, 虽然a是一个泛型类型, 但a必须实现CanToString类型类.

而任何实现CanToString类型类的类型都可以被toStr函数转换为字符串, 字符串就可以拼接了.

接下来就可以使用了:

x7_1 :: String
x7_1 = f7 1

x7_2 :: String
x7_2 = f7 true

但如果输入的值的类型没有实现CanToString, 就会报错了:

x7_3 :: String
x7_3 = f7 1.2

完整代码

module Main where

import Prelude
import Effect (Effect)
import Effect.Console (log)

class CanToString a where
  toStr :: a -> String

instance CanToString Int where
  toStr :: Int -> String
  toStr a = show a

instance CanToString Boolean where
  toStr :: Boolean -> String
  toStr true = "T"
  toStr false = "F"

f7 :: forall a. CanToString a => a -> String
f7 x = (toStr x) <> "_"

x7_1 :: String
x7_1 = f7 1

x7_2 :: String
x7_2 = f7 true

main :: Effect Unit
main = log x7_1

类型类的意义

多态

在日常生活中, 我们会混用一些动词.

例如, 把1和2加起来, 和, 把"a"和"b"加起来.

他们都用了这个动词, 但在两句话中, 它的意思是不一样的.

第一句话指的是数学上的加法, 第二句话指的则是字符串拼接.

这种一个词在不同的句子里有不同的行为的情况, 我们称为多态.

一个词的意义, 取决于它所在的句子, 要结合上下文来理解才行.

更明确的, 对于动词而言, 它的意义取决于它的参与者的类型.

在这个例子里, 的实际行为取决于其参与者是数字还是字符串.

用类型类描述多态

类型类可以方便的描述多态, 比如我定义一个多态动词, 攻击:

class Attack a b where
  attack :: a -> b -> b

攻击这个动词有两个参与者, 这里称为ab.

a是攻击者, b是被攻击者, 最后这个函数返回一个被攻击后的被攻击者.

现在, 定义玩家和敌人, 然后实现他们的攻击行为:

data Player
  = Player { hp :: Int, atk :: Int }

data Enemy
  = Enemy { hp :: Int, atk :: Int }
  
instance Attack Player Enemy where
  attack :: Player -> Enemy -> Enemy
  attack (Player { atk }) (Enemy e) = Enemy { hp: e.hp - atk, atk: e.atk }

instance Attack Enemy Player where
  attack :: Enemy -> Player -> Player
  attack (Enemy { atk: e_atk }) (Player { hp, atk: p_atk }) = Player { hp: hp - e_atk, atk: p_atk }

这样, 攻击这个词就是一个多态动词了.

它既可以输入玩家→敌人, 来表示玩家攻击敌人, 也可以输入敌人→玩家, 来表示敌人攻击玩家.

虽然我们都用的是attack这个函数, 但输入不同, 它执行的行为也就不同.

使用的例子:

player :: Player
player = Player { hp: 100, atk: 1 }

shiLaiMu :: Enemy
shiLaiMu = Enemy { hp: 10, atk: 1 }

-- shiLaiMu' 的hp是9
shiLaiMu' :: Enemy
shiLaiMu' = attack player shiLaiMu

-- player' 的hp是99
player' :: Player
player' = attack shiLaiMu' player

建模

可以看到, 我们通过类型, 类型类, 函数, 值, 一系列的东西, 形成了一个体系.

现实是无限复杂的, 我们需要舍弃其中的一部分, 建立一个虚拟的世界.

然后在这个虚拟的, 有限的世界中解决问题.

这称为建模.

我们需要有一些方法描述这个虚拟世界的规则, 什么是怎么组成的, 什么属于什么, 什么可以变成什么.

构建的基础工具就是这一套类型体系了.

其实建模的方式有很多种, 这只是一个方法, 感兴趣的话可以看看类型系统的相关书籍.

在上面的例子里, 我们描述了玩家, 敌人, 描述了他们如何攻击.

但玩家, 敌人这些概念本身并不存在于编程语言中, 而是存在于我们的脑中.

我们用类型和类型类描述了这一切.

类型, 类型类, 甚至任何一行代码都不是机械的, 告诉电脑怎么运行, 而是描述虚拟世界的一部分.

所以, 重点并不是去研究类型是怎么定义的, 函数是怎么实现的.

而是要理解, 构造这个类型, 类型类的人是怎么想的.

这也是学习这个语言最难的部分, 不要用机器的方法思考, 而是用模型的方法思考.