简体   繁体   English

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

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

In TypeScript, there are type-level functions that allow creating new types based on given literal types/specifications (see Mapped Types , Conditional Types , etc.).在 TypeScript 中,有一些类型级函数允许根据给定的文字类型/规范创建新类型(请参阅映射类型条件类型等)。

For instance, here is such a function , let say provided by a lib author:例如,这是一个function ,假设由 lib 作者提供:

type FromSpec<S> = { 
  [K in keyof S]: S[K] extends "foo" ? ExampleType : never 
};

Its purpose is, given a specification S in the form of a map of string keys and arbitrary literals, it creates a new type in the form of a map with the same set of keys and with values transformed.其目的是,给定字符串键和任意文字的 map 形式的规范S ,它以 map 的形式创建一个新类型,具有相同的键集和转换的值。 If a the value is the literal "foo" then it becomes the type ExampleType , otherwise the value is rejected by transforming it into the bottom type never .如果值是文字"foo" ,那么它变成类型ExampleType ,否则通过将值转换为底部类型never来拒绝该值。

Then, an end-user can make use of this function to create new types following the above explanation:然后,最终用户可以使用此 function 按照上述说明创建新类型:

type Example = FromSpec<{some_key: "foo", another_key: "bar"}>
//           = {some_key: ExampleType, another_key: never} 

It's noteworthy that the lib author doesn't know about what exact type a given end-user may want, and thus provides him with a function to create the ones he needs.值得注意的是,lib 作者不知道给定最终用户可能想要的确切类型,因此为他提供了 function 来创建他需要的类型。 On the other hand, the end-user can create an infinite set of new types as long as he complies with the function's capabilities.另一方面,最终用户可以创建无限的新类型集,只要他遵守函数的功能。

You can play around this simple example, here .你可以在这里玩这个简单的例子。


The question is about how this kind of "dynamism" is expressible in other typed languages (eg, ReasonML/OCaml, Scala, Haskell).问题在于这种“活力”如何在其他类型语言中表达(例如,ReasonML/OCaml、Scala、Haskell)。 Or how, as an end-user, to create new types, at compile-time , by using type-level functions, provided by a lib author (as one would usually do at runtime with value-level functions)?或者,作为最终用户,如何在编译时通过使用 lib 作者提供的类型级函数来创建新类型(就像在运行时通常使用值级函数所做的那样)?

It's important to note that the question is not about which language is better, etc. It's about finding the most straightforward and explicit way to express such capabilities.重要的是要注意,问题不在于哪种语言更好,等等。它是关于找到表达这种能力的最直接和明确的方式。 Here we saw an example in TypeScript, but is there any more natural way in any other language?在这里,我们在 TypeScript 中看到了一个示例,但是在任何其他语言中还有更自然的方式吗?

Given Scala is one of the tagged languages, here is a solution in Dotty (aka. Scala 3).鉴于 Scala 是标记语言之一,这是 Dotty 中的解决方案(又名 Scala 3)。 Take this with a grain of salt, since Dotty is still under development.对此持保留态度,因为 Dotty 仍在开发中。 Tested with Dotty version 0.24.0-RC1, here is a Scastie that proves this actually compiles .使用 Dotty 版本 0.24.0-RC1 进行测试,这里有一个 Scastie 证明这实际上可以编译

Scala doesn't have the same sort of built-in type machinery as TypeScript for manipulating records. Scala 没有与 TypeScript 相同类型的用于操作记录的内置类型机械。 Not to fear, we can roll our own!不要害怕,我们可以自己滚动!

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") *: ()

Then, FromSpec can be implemented using amatch-type .然后, FromSpec可以使用match-type来实现。 The never type in TypeScript is called Nothing in Scala/Dotty. 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
}

Finally, let's use FromSpec :最后,让我们使用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 (eg, ReasonML/OCaml, Scala, Haskell).是否可以用另一种类型的语言(例如,ReasonML/OCaml、Scala、Haskell)来表达相同类型的“动态”或接近它的东西。

Yes, dynamic types are fully supported by the OCaml/ReasonML type system and are widely used.是的,动态类型完全被 OCaml/ReasonML 类型系统支持并且被广泛使用。 You can express quite complex dynamic typing rules, eg, build your hierarchies, implement ad-hoc polymorphism and so on.您可以表达相当复杂的动态类型规则,例如,构建您的层次结构、实现临时多态性等等。 The main ingredients of the solution is using extensible GADT, first-class modules, and existentials.该解决方案的主要成分是使用可扩展的 GADT、一流的模块和存在主义。 See this answer 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.将此答案视为示例之一或此讨论, 以了解通用值的一般情况,还有多个库在 OCaml 中提供各种动态类型功能。 Another example is BAP's Core Theory library that has a very complex type hierarchy for value sorts, which includes precise type specifications for various number representations, including floating-point numbers, memories, etc.另一个例子是 BAP 的Core Theory库,它具有非常复杂的值排序类型层次结构,其中包括各种数字表示的精确类型规范,包括浮点数、内存等。

To make the answer complete, this is how you can implement your fromSpec in OCaml, first we define type that will be bearing the tag for dynamic typing, underneath the hood this is just an integer, but with associated type which it is witnessing ,为了使答案完整,这就是您可以在 OCaml 中实现fromSpec的方法,首先我们定义将带有动态类型标签的类型,在引擎盖下这只是一个 integer,但与它正在见证的关联类型,

type 'a witness = ..

To create a new witness (basically incrementing this id) we will use first class modules and append a new constructor using +=要创建一个新的见证(基本上是增加这个 id),我们将使用第一个 class 模块和 append 使用+=的新构造函数

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)

The type equality proof (the value that proofs to the compiler that two types are the same since they are both using the constructor with the same identity), is commonly represented as ('a,'b) eq type,类型相等证明(向编译器证明两种类型相同的值,因为它们都使用具有相同标识的构造函数),通常表示为('a,'b) eq类型,

type ('a,'b) eq = Equal : ('a,'a) eq

And this is how we implement the cast function,这就是我们实现演员 function 的方式,

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

finally, your fromSpec ,最后,您的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 

Disclaimer: I'm not a C++ programmer, so don't take this answer to be the proper way to do it in C++.免责声明:我不是 C++ 程序员,所以不要认为这个答案是在 C++ 中做这件事的正确方法。 It is just one way to do it that is extremely brittle and is probably mostly wrong.这只是一种非常脆弱的方法,而且可能大部分都是错误的。

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

Now you can use it like this:现在你可以像这样使用它:

typedef Cons<Named<field1, foo>, Cons<Named<field2, bar>, End>> C;

//Notice the "::type"
typedef FromSpec<C>::type T;

T is equivalent to Cons<ExampleType, Cons<void, End>> T等价于Cons<ExampleType, Cons<void, End>>

You can then access the types inside like so:然后你可以像这样访问里面的类型:

typedef T::type E; //Equivalent to ExampleType
typedef T::type::Next N; //Equivalent to Cons<void, End>
typedef N::type v; //Equivalent to void

Example usage示例用法

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

Prints "This is way too complicated!"打印“这太复杂了!”

Link to repl.it链接到 repl.it

Feel free to edit my answer if it has mistakes or if it could be otherwise improved.如果我的答案有错误或可以以其他方式改进,请随时编辑我的答案。 I mostly just tried to translate the answer by @Alec into C++.我主要只是试图将@Alec的答案翻译成C++

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM