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
攻击这个动词有两个参与者, 这里称为a
和b
.
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
可以看到, 我们通过类型, 类型类, 函数, 值, 一系列的东西, 形成了一个体系.
现实是无限复杂的, 我们需要舍弃其中的一部分, 建立一个虚拟的世界.
然后在这个虚拟的, 有限的世界中解决问题.
这称为建模.
我们需要有一些方法描述这个虚拟世界的规则, 什么是怎么组成的, 什么属于什么, 什么可以变成什么.
构建的基础工具就是这一套类型体系了.
其实建模的方式有很多种, 这只是一个方法, 感兴趣的话可以看看类型系统的相关书籍.
在上面的例子里, 我们描述了玩家, 敌人, 描述了他们如何攻击.
但玩家, 敌人这些概念本身并不存在于编程语言中, 而是存在于我们的脑中.
我们用类型和类型类描述了这一切.
类型, 类型类, 甚至任何一行代码都不是机械的, 告诉电脑怎么运行, 而是描述虚拟世界的一部分.
所以, 重点并不是去研究类型是怎么定义的, 函数是怎么实现的.
而是要理解, 构造这个类型, 类型类的人是怎么想的.
这也是学习这个语言最难的部分, 不要用机器的方法思考, 而是用模型的方法思考.