如何使用类型级函数动态创建静态类型?

How to use type-level functions to create static types, dynamically?

本文关键字:类型 静态 静态类 创建 函数 何使用 动态      更新时间:2023-10-16

在 TypeScript 中,有一些类型函数允许根据给定的文本类型/规范创建新类型(请参阅映射类型、条件类型等(。

例如,这里有这样一个函数,假设由 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 作者不知道给定最终用户可能想要的确切类型,因此为他提供了一个创建他需要的类型的功能。另一方面,最终用户可以创建无限的新类型集,只要他符合函数的功能。

你可以在这里玩这个简单的例子。


问题是这种"动态"如何在其他类型语言中表达(例如,ReasonML/OCaml,Scala,Haskell(。或者,作为最终用户,如何在编译时使用 lib 作者提供的类型级函数在编译时创建新类型(就像人们通常在运行时使用值级函数所做的那样(?

重要的是要注意,问题不在于哪种语言更好,等等。这是关于找到最直接和明确的方式来表达这种能力。在这里,我们在 TypeScript 中看到了一个例子,但是在任何其他语言中还有更自然的方式吗?

鉴于 Scala 是标记语言之一,这里是 Dotty 中的一个解决方案(又名。斯卡拉 3(。对此持保留态度,因为Dotty仍在开发中。使用 Dotty 版本 0.24.0-RC1 进行测试,这里有一个 Scastie 证明这实际上是编译的。

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。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

是否有可能用另一种类型语言(例如,ReasonML/OCaml、Scala、Haskell(表达相同的"动态"或与之接近的东西。

是的,动态类型完全受OCaml/ReasonML类型系统支持,并且被广泛使用。你可以表达相当复杂的动态类型规则,例如,构建你的层次结构,实现临时多态性等等。该解决方案的主要成分是使用可扩展的 GADT、一流模块和存在。请参阅此答案作为示例之一或此讨论 对于通用值的一般情况,还有多个库在 OCaml 中提供各种动态类型功能。另一个例子是BAP的核心理论库,它有一个非常复杂的值排序类型层次结构,其中包括各种数字表示的精确类型规范,包括浮点数、存储器等。

为了使答案完整,这就是如何在 OCaml 中实现您的fromSpec,首先我们定义将带有动态类型标记的类型,在引擎盖下这只是一个整数,但具有它见证的关联类型,

type 'a witness = ..

为了创建一个新的见证(基本上是递增这个id(,我们将使用第一类模块并使用+=附加一个新的构造函数

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

这就是我们实现 cast 函数的方式,

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;
}

打印"这太复杂了!">

链接到 repl.it

如果答案有错误或可以改进,请随时编辑我的答案。我主要只是试图通过@Alec将答案翻译成C++。