内容很大部分来自 https://haskell.love/richard-eisenberg/ 这一视频,奇怪的知识增加了。
引入
1 | const :: a -> b -> a |
const 函数接收几个参数呢?是 a 类型的 x 与 b 类型的 y —— 两个吗?
为了便捷,GHC 默认会省略掉 rank 1 的 quantifier。我们可以打开语言扩展ExplicitForAll 并在 GHCi中开启参数 -fprint-explicit-foralls,以便在代码中可以显式书写量词,以及使得在 GHCi 使用 :t 时会同时打印出量词。当然,有了 UnicodeSyntax 语言扩展,∀ 也能被 parse。所以我们可以改写一下新的 const:
1 | const :: ∀ a b . a -> b -> a |
这样就清晰多了 —— const 接收四个参数:类型变量 a 和 b,以及 x 和 y。有的同志(用过 exteff 相关库)可能知道一个叫 TypeApplications 的扩展,开启它之后类型变量也能显式指定,例如 const @Int @String 233 "QAQ"。这样以来我们可以更加确信 const 有四个参数。
再来看一个相似的例子:
1 | shout :: ∀ a . Show a => a -> String |
shout 接收三个参数:类型变量 a、类型类实例 Show a 以及 x。类型类实例有点像隐式转换,编译器会在上下文中去找这个东西,使得函数体中能够正确调用类型类约定的函数。值得注意的是和类型变量不同,类型类实例只能靠编译器推断,不能手动传进去。
三类形参
下面介绍三种形参具有的特性:dependent、relevant、visible。
- dependent —— 这个参数是依赖的,即这个参数值的选择会影响后续参数的类型。
- relevant —— 这个参数是相关的,即这个参数值能在运行期函数体中使用。
- visible —— 这个参数是可见的,即这个参数能显式传入。
这样我们可以画出一张表:
| 类型变量 | 类型类实例 | 常规项 | |
|---|---|---|---|
| dependent | 是 | 否 | 否 |
| relevant | 否 | 是 | 是 |
| visible | 否 | 否 | 是 |
通过上面 const 与 shout 的例子,我们得知了:
常规项,例如
Int -> ...是 relevant(参数就要在函数体用嘛)、visible*(当然要显式传,不然怎么知道是几)并且 *non-dependent 的(你 Haskell 并没有原生依赖类型)。类型类实例,例如
Show a => ...是 relevant(函数体调用到了类型类实例中的代码)、invisible (只能靠编译器找)并且 non-dependent 的。类型变量,例如
∀ (a :: Type). ...是 irrelevant、invisible 并且 non-dependent 的。
事实上,这三类特性还有其他的组合方式,我们暂且画出一张更全的表:
| relevant | visible | dependent | 表示方法 |
|---|---|---|---|
| 是 | 是 | 是 | ????? |
| 是 | 是 | 否 | 通常项 Int -> ... |
| 是 | 否 | 是 | ????? |
| 是 | 否 | 否 | 类型类实例 Show a => ... |
| 否 | 是 | 是 | ????? |
| 否 | 是 | 否 | |
| 否 | 否 | 是 | 类型参数 ∀ (a :: Type). ... |
| 否 | 否 | 否 |
不要急,咱一点点来,先从 relevant 看起。
relevant
看看以下 id 函数:
1 | -- 这段代码不能成功编译 |
我们尝试定义出一个奇怪的 id 函数,对于 Bool 类型的 True 会返回 False;而对于其他类型则原封不动地返回。好像没啥问题的说,但这段代码并不能被 GHC 编译。id 应适用于所有类型 a,但我们在第一个模式中把 a 特化成了 Bool,这是不合法的。由此可以得出 Haskell 中的类型变量是 irrelevant 的(至少在编译期)。但是!咱还有所谓的反射(Data.Typeable / Type.Reflection):
1 | weirdID :: ∀ a . Typeable a => a -> a |
Pattern guard 语法之前没见过呢。 不难看出,这里在运行期比较了 a 和 Bool 是否相等,根据比较结果来决定函数行为。Typeable constraint 给了类型 Propositional equality 的能力。weirdID 中的 a 已经算是 dependent & relevant 了,虽然有些丑陋。至于说这个组合有什么用,别问,问就是模拟依赖类型。当然,还可以像 weirdID2 这样显式传入 TypeRep 来代替 Typeable 约束,它们没啥本质上的区别:
1 | weirdID2 :: ∀ a . TypeRep a -> a -> a |
visible
weirdID / weirdID2 中的 a 已经做到了 dependent & relevant*,那么 *visible 呢?很遗憾,universal quantification ∀ a 依然是不可见的,而我们真正想要的是 VDQ(visible dependent quantification),就像 weirdID3 这样:
1 | -- 这段代码不能成功编译 |
这里的 ∀ a. 变成了 ∀ a ->,意味着 ∀ a 从一个 Scheme(在 universal quantifier 约束下的 polymorphic type variable)变成了一个 dependent term。
只不过 —— 编译这段代码,你会得到 Illegal visible, dependent quantification in the type of a term: forall a -> Typeable a => a -> a (GHC does not yet support this)(这波啊,这波是 GHC 布星
既然 Term 层不行,咱来 Kind 层实现:
1 | type WeirdId3 :: ∀ a -> a -> a -- 需要打开 StandaloneKindSignatures |
唔姆,类型家族很好理解,就是定义一个操作类型的函数。在 GHCi 中运行 :k WeirdId3 Bool True 可以得到 'False。注意这个 ' —— 咱运算得出的结果是一个类型,'False 的 kind 是 Bool。类似,运行 WeirdId3 Symbol "hello" 可以得到 "hello",当然这个 "hello" 不是 String 的实例,而是 kind 为 Symbol 的一个类型。所有在 Kind Level 进行 quantification 的类型变量都是 relevant 的,这是 GHC 钦定的。换句话说,咱写不出来类型家族 WeirdId3 :: ∀ a. a -> a,而且调用 WeirdId _ x 时 _ 必须写上 x 的类型。
好像完事了,来把表填上:
| relevant | visible | dependent | 表示方法 |
|---|---|---|---|
| 是 | 是 | 是 | Typeable VDQ: ∀ a -> Typeable a => ...(GHC 现在不支持)类型层 VDQ: ∀ a -> ... |
| 是 | 是 | 否 | 通常项: Int -> ...类型层: ℕ -> ... |
| 是 | 否 | 是 | 有 Typeable 约束的类型参数 ∀ a . Typeable a =>(恶星)类型层: ∀ a . ... |
| 是 | 否 | 否 | 类型类实例 Show a => ... |
| 否 | 是 | 是 | VDQ:∀ a -> ...(GHC 现在不支持 |
| 否 | 是 | 否 | |
| 否 | 否 | 是 | 类型参数 ∀ (a :: Type). ... |
| 否 | 否 | 否 |
看到这儿相信大家应该能理解 ∀ a. ... 和 ∀ a -> ... 的区别吧……
题外话
看这个 Vec(Indexed datatype):
1 | data ℕ = Z | S ℕ |
对于 vReplicate 而言,长度 n(类型变量)既是 relevant 的又是 dependent 的,因为这个函数要根据 n 来确定生成多长的 Vec,同时 Vec 的类型也依赖它的长度。事实上现在咱们还没法使用 ℕ 进行运算。首先,它是个 Type,咱们需要 type family 来定义操作 ℕ。其次,有了 type family 后它也只能出现在类型签名里或者像 vReplicate 这样通过 Typeable 强行让 n 变得 relevant,不能直接在模式匹配中使用。强制 Typeable 是很恶星的,把事情都搞到了 runtime,还弱化了类型系统的表达能力。关于更好地在 Haskell 中使用这种索引数据类型,可以康康 singleton,顺便做一下 Codewars 上那道题(
Further Reading
Linear Types(在 GHC 9.0 就能用啦~)