如何使用类型级函数动态创建静态类型?
How to use type-level functions to create static types, dynamically?
在 TypeScript 中,有 type-level functions 允许基于给定的 literal types/specifications(参见Mapped Types, Conditional Types等)。
例如,这里有这样一个函数,假设是lib作者提供的:
type FromSpec<S> = {
[K in keyof S]: S[K] extends "foo" ? ExampleType : never
};
它的目的是,给定一个字符串键和任意文字映射形式的规范S
,它以具有相同键和值集的映射形式创建一个新类型转变。如果值是字面量 "foo"
则它变成类型 ExampleType
,否则通过将其转换为底层类型 never
.
拒绝该值
然后,最终用户可以使用此功能按照上述说明创建新类型:
type Example = FromSpec<{some_key: "foo", another_key: "bar"}>
// = {some_key: ExampleType, another_key: never}
值得注意的是,lib 作者不知道给定的最终用户可能想要什么类型,因此为他提供了创建他需要的类型的功能。另一方面,最终用户只要遵守函数的功能,就可以创建无限组新类型。
你可以玩这个简单的例子,here。
问题是关于这种 "dynamism" 如何用其他类型语言(例如 ReasonML/OCaml、Scala、Haskell)表达。或者,作为最终用户,如何使用库作者提供的类型级函数在编译时创建新类型(就像人们通常在运行时使用值级函数)?
重要的是要注意,问题不是关于哪种语言更好等问题。而是关于找到最直接和明确的方式来表达这种能力。在这里我们看到了 TypeScript 中的示例,但是在任何其他语言中是否还有更多 natural 方式?
鉴于 Scala 是标记语言之一,这里是 Dotty(又名 Scala 3)中的解决方案。对此持保留态度,因为 Dotty 仍在开发中。使用 Dotty 版本 0.24.0-RC1 测试,这里是 a Scastie that proves this actually compiles.
Scala 没有与 TypeScript 相同的内置类型机制来处理记录。不要害怕,我们可以自己动手!
import deriving._
// A field is literally just a tuple of field name and value
type Field[K, V] = (K, V)
// This just helps type-inference infer singleton types in the right places
def field[K <: String with Singleton, V <: Singleton](
label: K,
value: V
): Field[K, V] = label -> value
// Here is an example of some records
val myRec1 = ()
val myRec2 = field("key1", "foo") *: field("key2", "foo") *: ()
val myRec3 =
field("key1", 1) *: field("key2", "foo") *: field("key3", "hello world") *: ()
然后,FromSpec
可以使用match-type来实现。 TypeScript 中的 never
类型在 Scala/Dotty.
中称为 Nothing
// Could be defined to be useful - `trait` is just an easy way to bring a new type in
trait ExampleType
val exampleValue = new ExampleType {}
type FromSpec[S <: Tuple] <: Tuple = S match {
case Field[k, "foo"] *: rest => Field[k, ExampleType] *: FromSpec[rest]
case Field[k, v] *: rest => Field[k, Nothing] *: FromSpec[rest]
case Unit => Unit
}
最后,让我们使用FromSpec
:
def myRec1Spec: FromSpec[myRec1.type] = ()
def myRec2Spec: FromSpec[myRec2.type] =
field("key1", exampleValue) *: field("key2", exampleValue) *: ()
def myRec3Spec: FromSpec[myRec3.type] = ??? // no non-diverging implementation
Is it possible to express the same kind of "dynamism" or something close to it in another typed language (e.g., ReasonML/OCaml, Scala, Haskell).
是的,OCaml/ReasonML 类型系统完全支持动态类型并被广泛使用。您可以表达相当复杂的动态类型规则,例如,构建您的层次结构、实现临时多态性等。该解决方案的主要成分是使用可扩展的 GADT、first-class 模块和存在性。请参阅此 as one of the example or this discussion for the general case of universal values, there are also multiple libraries that provide various dynamic typing capabilities in OCaml. Another example is BAP's Core Theory 库,它具有非常复杂的值排序类型层次结构,其中包括各种数字表示的精确类型规范,包括浮点数、内存等。
为了使答案完整,这就是你如何在 OCaml 中实现你的 fromSpec
,首先我们定义将带有动态类型标签的类型,在引擎盖下这只是一个整数,但是关联类型是 witnessing,
type 'a witness = ..
要创建一个新的见证人(基本上是递增这个 id),我们将首先使用 class 模块并使用 +=
附加一个新的构造函数
module type Witness = sig
type t
type _ witness += Id : t witness
end
type 'a typeid = (module Witness with type t = 'a)
let newtype (type u) () =
let module Witness = struct
type t = u
type _ witness += Id : t witness
end in
(module Witness : Witness with type t = u)
类型相等性证明(向编译器证明两种类型相同的值,因为它们都使用具有相同身份的构造函数),通常表示为 ('a,'b) eq
类型,
type ('a,'b) eq = Equal : ('a,'a) eq
这就是我们实现转换函数的方式,
let try_cast : type a b. a typeid -> b typeid -> (a,b) eq option =
fun x y ->
let module X : Witness with type t = a = (val x) in
let module Y : Witness with type t = b = (val y) in
match X.Id with
| Y.Id -> Some Equal
| _ -> None
最后,你的 fromSpec
、
type spec {
data : 'a;
rtti : 'a typeid
}
let example_type = newtype ()
let example = {
data = 42;
rtti = example_type; (* witnesses that data is `int` *)
}
let fromSpec = try_cast example_type
免责声明:我不是 C++ 程序员,所以不要将此答案视为在 C++ 中执行此操作的正确方法。这只是一种非常脆弱的方法,可能大部分都是错误的。
//I've used char pointers below, because it's not possible to directly write string //literals in templates without doing some more complex stuff that isn't relevant here
//field1 and field2 are the names of the fields/keys
const char field2[] = "field2";
const char field1[] = "field1";
//foo and bar are the strings that determine what the
//type of the fields will be
const char foo[] = "foo";
const char bar[] = "bar";
//This represents a key and the determining string (foo/bar)
template <const char * name, const char * det>
struct Named {};
//What the type of the field will be if it maps to "foo"
struct ExampleType {
std::string msg;
};
//The end of a cons structure
struct End{};
//A cons-like structure, but for types
template <typename T, typename N>
struct Cons {
typedef T type;
typedef N Next;
};
//This'll be used to create new types
//While it doesn't return a type, per se, you can access the
//"created" type using "FromSpec<...>::type" (see below)
template <typename T>
struct FromSpec;
//This will handle any Named template where the determining string
//is not "foo", and gives void instead of ExampleType
template <const char * name, const char * det, typename rest>
struct FromSpec<Cons<Named<name, det>, rest>> {
//Kinda uses recursion to find the type for the rest
typedef Cons<void, typename FromSpec<rest>::type> type;
};
//This will handle cases when the string is "foo"
//The first type in the cons is ExampleType, along with the name
//of the field
template <const char * name, typename rest>
struct FromSpec<Cons<Named<name, foo>, rest>> {
typedef Cons<ExampleType, typename FromSpec<rest>::type> type;
};
//This deals with when you're at the end
template <>
struct FromSpec<End> {
typedef End type;
};
现在你可以这样使用了:
typedef Cons<Named<field1, foo>, Cons<Named<field2, bar>, End>> C;
//Notice the "::type"
typedef FromSpec<C>::type T;
T
等同于 Cons<ExampleType, Cons<void, End>>
然后您可以像这样访问里面的类型:
typedef T::type E; //Equivalent to ExampleType
typedef T::type::Next N; //Equivalent to Cons<void, End>
typedef N::type v; //Equivalent to void
用法示例
int main() {
ExampleType et = { "This is way too complicated!" };
//You can kinda have values of type "void", unfortunately,
//but they're really just null
// v
N inner = { nullptr, new End() };
T obj = { &et, &inner };
Cons<ExampleType, Cons<void, End>> obj2 = obj;
std::cout << et.msg << std::endl;
}
打印 "This is way too complicated!"
如果我的答案有错误或者可以通过其他方式改进,请随时编辑我的答案。我主要只是试图将 by @Alec 翻译成 C++。
在 TypeScript 中,有 type-level functions 允许基于给定的 literal types/specifications(参见Mapped Types, Conditional Types等)。
例如,这里有这样一个函数,假设是lib作者提供的:
type FromSpec<S> = {
[K in keyof S]: S[K] extends "foo" ? ExampleType : never
};
它的目的是,给定一个字符串键和任意文字映射形式的规范S
,它以具有相同键和值集的映射形式创建一个新类型转变。如果值是字面量 "foo"
则它变成类型 ExampleType
,否则通过将其转换为底层类型 never
.
然后,最终用户可以使用此功能按照上述说明创建新类型:
type Example = FromSpec<{some_key: "foo", another_key: "bar"}>
// = {some_key: ExampleType, another_key: never}
值得注意的是,lib 作者不知道给定的最终用户可能想要什么类型,因此为他提供了创建他需要的类型的功能。另一方面,最终用户只要遵守函数的功能,就可以创建无限组新类型。
你可以玩这个简单的例子,here。
问题是关于这种 "dynamism" 如何用其他类型语言(例如 ReasonML/OCaml、Scala、Haskell)表达。或者,作为最终用户,如何使用库作者提供的类型级函数在编译时创建新类型(就像人们通常在运行时使用值级函数)?
重要的是要注意,问题不是关于哪种语言更好等问题。而是关于找到最直接和明确的方式来表达这种能力。在这里我们看到了 TypeScript 中的示例,但是在任何其他语言中是否还有更多 natural 方式?
鉴于 Scala 是标记语言之一,这里是 Dotty(又名 Scala 3)中的解决方案。对此持保留态度,因为 Dotty 仍在开发中。使用 Dotty 版本 0.24.0-RC1 测试,这里是 a Scastie that proves this actually compiles.
Scala 没有与 TypeScript 相同的内置类型机制来处理记录。不要害怕,我们可以自己动手!
import deriving._
// A field is literally just a tuple of field name and value
type Field[K, V] = (K, V)
// This just helps type-inference infer singleton types in the right places
def field[K <: String with Singleton, V <: Singleton](
label: K,
value: V
): Field[K, V] = label -> value
// Here is an example of some records
val myRec1 = ()
val myRec2 = field("key1", "foo") *: field("key2", "foo") *: ()
val myRec3 =
field("key1", 1) *: field("key2", "foo") *: field("key3", "hello world") *: ()
然后,FromSpec
可以使用match-type来实现。 TypeScript 中的 never
类型在 Scala/Dotty.
Nothing
// Could be defined to be useful - `trait` is just an easy way to bring a new type in
trait ExampleType
val exampleValue = new ExampleType {}
type FromSpec[S <: Tuple] <: Tuple = S match {
case Field[k, "foo"] *: rest => Field[k, ExampleType] *: FromSpec[rest]
case Field[k, v] *: rest => Field[k, Nothing] *: FromSpec[rest]
case Unit => Unit
}
最后,让我们使用FromSpec
:
def myRec1Spec: FromSpec[myRec1.type] = ()
def myRec2Spec: FromSpec[myRec2.type] =
field("key1", exampleValue) *: field("key2", exampleValue) *: ()
def myRec3Spec: FromSpec[myRec3.type] = ??? // no non-diverging implementation
Is it possible to express the same kind of "dynamism" or something close to it in another typed language (e.g., ReasonML/OCaml, Scala, Haskell).
是的,OCaml/ReasonML 类型系统完全支持动态类型并被广泛使用。您可以表达相当复杂的动态类型规则,例如,构建您的层次结构、实现临时多态性等。该解决方案的主要成分是使用可扩展的 GADT、first-class 模块和存在性。请参阅此
为了使答案完整,这就是你如何在 OCaml 中实现你的 fromSpec
,首先我们定义将带有动态类型标签的类型,在引擎盖下这只是一个整数,但是关联类型是 witnessing,
type 'a witness = ..
要创建一个新的见证人(基本上是递增这个 id),我们将首先使用 class 模块并使用 +=
module type Witness = sig
type t
type _ witness += Id : t witness
end
type 'a typeid = (module Witness with type t = 'a)
let newtype (type u) () =
let module Witness = struct
type t = u
type _ witness += Id : t witness
end in
(module Witness : Witness with type t = u)
类型相等性证明(向编译器证明两种类型相同的值,因为它们都使用具有相同身份的构造函数),通常表示为 ('a,'b) eq
类型,
type ('a,'b) eq = Equal : ('a,'a) eq
这就是我们实现转换函数的方式,
let try_cast : type a b. a typeid -> b typeid -> (a,b) eq option =
fun x y ->
let module X : Witness with type t = a = (val x) in
let module Y : Witness with type t = b = (val y) in
match X.Id with
| Y.Id -> Some Equal
| _ -> None
最后,你的 fromSpec
、
type spec {
data : 'a;
rtti : 'a typeid
}
let example_type = newtype ()
let example = {
data = 42;
rtti = example_type; (* witnesses that data is `int` *)
}
let fromSpec = try_cast example_type
免责声明:我不是 C++ 程序员,所以不要将此答案视为在 C++ 中执行此操作的正确方法。这只是一种非常脆弱的方法,可能大部分都是错误的。
//I've used char pointers below, because it's not possible to directly write string //literals in templates without doing some more complex stuff that isn't relevant here
//field1 and field2 are the names of the fields/keys
const char field2[] = "field2";
const char field1[] = "field1";
//foo and bar are the strings that determine what the
//type of the fields will be
const char foo[] = "foo";
const char bar[] = "bar";
//This represents a key and the determining string (foo/bar)
template <const char * name, const char * det>
struct Named {};
//What the type of the field will be if it maps to "foo"
struct ExampleType {
std::string msg;
};
//The end of a cons structure
struct End{};
//A cons-like structure, but for types
template <typename T, typename N>
struct Cons {
typedef T type;
typedef N Next;
};
//This'll be used to create new types
//While it doesn't return a type, per se, you can access the
//"created" type using "FromSpec<...>::type" (see below)
template <typename T>
struct FromSpec;
//This will handle any Named template where the determining string
//is not "foo", and gives void instead of ExampleType
template <const char * name, const char * det, typename rest>
struct FromSpec<Cons<Named<name, det>, rest>> {
//Kinda uses recursion to find the type for the rest
typedef Cons<void, typename FromSpec<rest>::type> type;
};
//This will handle cases when the string is "foo"
//The first type in the cons is ExampleType, along with the name
//of the field
template <const char * name, typename rest>
struct FromSpec<Cons<Named<name, foo>, rest>> {
typedef Cons<ExampleType, typename FromSpec<rest>::type> type;
};
//This deals with when you're at the end
template <>
struct FromSpec<End> {
typedef End type;
};
现在你可以这样使用了:
typedef Cons<Named<field1, foo>, Cons<Named<field2, bar>, End>> C;
//Notice the "::type"
typedef FromSpec<C>::type T;
T
等同于 Cons<ExampleType, Cons<void, End>>
然后您可以像这样访问里面的类型:
typedef T::type E; //Equivalent to ExampleType
typedef T::type::Next N; //Equivalent to Cons<void, End>
typedef N::type v; //Equivalent to void
用法示例
int main() {
ExampleType et = { "This is way too complicated!" };
//You can kinda have values of type "void", unfortunately,
//but they're really just null
// v
N inner = { nullptr, new End() };
T obj = { &et, &inner };
Cons<ExampleType, Cons<void, End>> obj2 = obj;
std::cout << et.msg << std::endl;
}
打印 "This is way too complicated!"
如果我的答案有错误或者可以通过其他方式改进,请随时编辑我的答案。我主要只是试图将