理解`GHC.TypeLits`
Understanding `GHC.TypeLits`
我正在努力思考 GHC 扩展 KindSignatures
和 DataKinds
。看Data.Modular这个包,大概明白了
newtype i `Mod` (n :: Nat) = Mod i deriving (Eq, Ord)
有点等同于声明一个 c++ 模板 <typename T, int N>
(构造函数只采用一个 T
类型的参数)。但是,查看 GHC.TypeLits 包,我不明白发生了什么。关于这个包的任何一般解释都会有所帮助。不过,在这个问题被标记为题外话之前,这里有一些具体的子问题:
- a
KnownNat
class 是有道理的,需要的函数可以让你从类型中提取类型变量,但是 natVal
做什么,[=18= 是什么]类型变量?
- 你会在哪里使用
someNatVal
?
- 最后,什么是
SomeNat
- 类型级别编号怎么可能是未知的? 是 的类型级别编号的全部意义不是在编译时已知的吗?
这个问题很宽泛 -- 我只谈几点。
proxy
类型变量只是* -> *
类型的类型变量,即类型构造函数的种类。务实地说,如果你有一个函数
foo :: proxy a -> ...
您可以将类型的值传递给它,例如Maybe Int
,选择 proxy = Maybe
和 a = Int
。您还可以传递 [] Char
类型的值(也写为 [Char]
)。或者,更常见的是 Proxy Int
类型的值,其中 Proxy
是定义为
的数据类型
data Proxy a = Proxy
即一种不携带任何 运行 时间信息的数据类型(它只有一个值!),但携带编译时信息(phantom 类型变量 a
).
假设 N
是一种类型 Nat
-- 编译时自然。我们可以写一个函数
bar :: N -> ...
但是调用它需要我们构建一个 N
类型的值——这是无关紧要的。 type N
的目的只是为了携带编译时信息,而它的 运行-time 值并不是我们真正想要使用的东西。事实上,N
除了 bottom 之外根本没有任何值。我们可以调用
bar (undefined :: N)
但这看起来很奇怪。读到这里,我们必须意识到 bar
在它的第一个参数中是惰性的,并且它不会导致尝试使用它的分歧。问题在于 bar :: N -> ...
类型签名具有误导性:它声称结果可能取决于类型 N
的参数值,但事实并非如此。相反,如果我们使用
baz :: Proxy N -> ...
意图很明确——只有一个 运行 时间值:Proxy :: Proxy N
。同样清楚的是 N
值仅在编译时出现。
有时,不是使用特定的 Proxy N
,而是将代码稍微概括为
foo :: proxy N -> ...
实现相同的目标,但也允许不同的 Proxy
类型。 (就个人而言,我对这种概括并不感到非常兴奋。)
回到问题:natVal
是一个将编译时纯自然值转换为 运行 时值的函数。 IE。它将 Proxy N
转换为 Int
,仅返回常量。
如果您使用 type 模板参数来模拟编译时自然值,您与 C++ 模板的类比可能会更接近。例如
template <typename N> struct S { using pred = N; };
struct Z {};
template <typename N> int natVal();
template <typename N> int natVal() { return 1 + natVal<typename N::pred>(); }
template <> int natVal<Z>() { return 0; }
int main() {
cout << natVal<S<S<Z>>>() << endl; // outputs 2
return 0;
}
假装 S
和 Z
没有 public 构造函数:它们的 运行 时间值并不重要,只有它们的编译时信息才重要。
我正在努力思考 GHC 扩展 KindSignatures
和 DataKinds
。看Data.Modular这个包,大概明白了
newtype i `Mod` (n :: Nat) = Mod i deriving (Eq, Ord)
有点等同于声明一个 c++ 模板 <typename T, int N>
(构造函数只采用一个 T
类型的参数)。但是,查看 GHC.TypeLits 包,我不明白发生了什么。关于这个包的任何一般解释都会有所帮助。不过,在这个问题被标记为题外话之前,这里有一些具体的子问题:
- a
KnownNat
class 是有道理的,需要的函数可以让你从类型中提取类型变量,但是natVal
做什么,[=18= 是什么]类型变量? - 你会在哪里使用
someNatVal
? - 最后,什么是
SomeNat
- 类型级别编号怎么可能是未知的? 是 的类型级别编号的全部意义不是在编译时已知的吗?
这个问题很宽泛 -- 我只谈几点。
proxy
类型变量只是* -> *
类型的类型变量,即类型构造函数的种类。务实地说,如果你有一个函数
foo :: proxy a -> ...
您可以将类型的值传递给它,例如Maybe Int
,选择 proxy = Maybe
和 a = Int
。您还可以传递 [] Char
类型的值(也写为 [Char]
)。或者,更常见的是 Proxy Int
类型的值,其中 Proxy
是定义为
data Proxy a = Proxy
即一种不携带任何 运行 时间信息的数据类型(它只有一个值!),但携带编译时信息(phantom 类型变量 a
).
假设 N
是一种类型 Nat
-- 编译时自然。我们可以写一个函数
bar :: N -> ...
但是调用它需要我们构建一个 N
类型的值——这是无关紧要的。 type N
的目的只是为了携带编译时信息,而它的 运行-time 值并不是我们真正想要使用的东西。事实上,N
除了 bottom 之外根本没有任何值。我们可以调用
bar (undefined :: N)
但这看起来很奇怪。读到这里,我们必须意识到 bar
在它的第一个参数中是惰性的,并且它不会导致尝试使用它的分歧。问题在于 bar :: N -> ...
类型签名具有误导性:它声称结果可能取决于类型 N
的参数值,但事实并非如此。相反,如果我们使用
baz :: Proxy N -> ...
意图很明确——只有一个 运行 时间值:Proxy :: Proxy N
。同样清楚的是 N
值仅在编译时出现。
有时,不是使用特定的 Proxy N
,而是将代码稍微概括为
foo :: proxy N -> ...
实现相同的目标,但也允许不同的 Proxy
类型。 (就个人而言,我对这种概括并不感到非常兴奋。)
回到问题:natVal
是一个将编译时纯自然值转换为 运行 时值的函数。 IE。它将 Proxy N
转换为 Int
,仅返回常量。
如果您使用 type 模板参数来模拟编译时自然值,您与 C++ 模板的类比可能会更接近。例如
template <typename N> struct S { using pred = N; };
struct Z {};
template <typename N> int natVal();
template <typename N> int natVal() { return 1 + natVal<typename N::pred>(); }
template <> int natVal<Z>() { return 0; }
int main() {
cout << natVal<S<S<Z>>>() << endl; // outputs 2
return 0;
}
假装 S
和 Z
没有 public 构造函数:它们的 运行 时间值并不重要,只有它们的编译时信息才重要。