【问题标题】:How to use type-level functions to create static types, dynamically?如何使用类型级函数动态创建静态类型?
【发布时间】:2020-06-11 05:20:28
【问题描述】:

在 TypeScript 中,有 type-level 函数 允许根据给定的 literal types/specifications 创建新类型em>(请参阅Mapped TypesConditional 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


问题在于如何在其他类型语言(例如 ReasonML/OCaml、Scala、Haskell)中表达这种“活力”。或者,作为最终用户,如何在编译时通过使用 lib 作者提供的类型级别函数来创建新类型(就像通常在运行时使用值级别所做的那样函数)?

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

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • @user 完成!请随时问我更精确。我是一个外行,我的词汇可能不是最特定领域的词汇。
  • @fsenart 您希望我奖励哪个答案,Alec 的答案还是 ivg 的答案?尽管有赏金,但似乎没有其他人会回答你的问题。
  • @user 恕我直言,Alec 的回答是表达这些想法的最明确和可扩展的方式。 Scala 3 将类型级编程提升到语言中一等公民的地位,并简化了整体推理。
  • @fsenart 好的,我会在星期一将赏金奖励给 Alec

标签: c++ typescript scala haskell ocaml


【解决方案1】:

鉴于 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

【讨论】:

  • 非常感谢您的精彩回答。问题的代码有一个区别:在 TypeScript 中,我对字段的值进行模式匹配,而不是它的名称(S[K] 不是 K)。因此,如果某个键的值是文字“foo”,则使用该键和类型ExampleType 生成一个类型。我想我知道如何根据您的回答来做到这一点,但如果您可以编辑并考虑到这一点,那就太棒了。谢谢。
  • 哎呀!我不精通 TypeScript。不过我会解决的。
【解决方案2】:

是否可以用另一种类型语言(例如 ReasonML/OCaml、Scala、Haskell)来表达相同类型的“动态”或类似的东西。

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

为了使答案完整,这就是您在 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

这就是我们实现强制转换功能的方式,

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 

【讨论】:

  • 非常感谢您的广泛回答。这是访问运行时类型信息的一种简洁模式。但是,我们是否同意在 OCaml 中我们不能在编译时拥有这种灵活性?我的问题并不准确,但是当我谈论类型级编程时,我是在编译时隐式谈论的。
  • 我还可以建议try_cast 是真正的fromSpec。事实上,您的 spec 是“具有运行时类型信息的值”的类型,应该命名为 rt_typed_value,不是吗?然后改为let spec = fromSpec example_type(即利用fromSpec 生成专门用于example_type 的动态规范)。此外,我是否可以使用不仅是int 的见证类型详细说明您的示例。实际上,由于它是一种专门用于使用的弱多态类型,因此可能有点难以理解如何同时拥有string 数据和int 数据。
  • 我冒昧地添加了更多示例和细节。请随时根据需要重新组织。再次感谢您富有洞察力的回答。
  • 我有点困惑。类型相等证明是否给我们动态类型的静态类型错误?该死的,感觉很神奇!
  • @fsenart,我想我看错了你的打字稿代码。为了创建静态类型,我们当然有类型构造函数和仿函数,后者非常强大(基本上是依赖类型),您可以使用它们编写 Girard 悖论。 github.com/lpw25/girards-paradox/blob/master/girard.ml鉴于此,您能否在更抽象和笼统的层面上解释一下,您在寻找什么?
【解决方案3】:

免责声明:我不是 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&lt;ExampleType, Cons&lt;void, End&gt;&gt;

然后你可以像这样访问里面的类型:

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

打印“这太复杂了!”

Link to repl.it

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

【讨论】:

  • 非常感谢您的回答。正如您所说,它遵循与@alec 答案几乎相同的模式匹配。请注意,ExampleType 只是一个示例类型,不一定与示例中的字段名称相关。您能否还提供一个使用创建的类型声明的变量的简单示例及其随附的示例值,以使解释完整。
  • @fsenart 添加了示例用法。这是非常基本的,但我怀疑你需要更详细的解释。希望对您有所帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-02
  • 2011-12-21
  • 2011-10-29
  • 1970-01-01
  • 2015-06-29
  • 1970-01-01
相关资源
最近更新 更多