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

做~自己de王妃 提交于 2021-02-18 17:39:14

问题


In TypeScript, there are type-level functions that allow creating new types based on given literal types/specifications (see Mapped Types, Conditional Types, etc.).

For instance, here is such a function, let say provided by a lib author:

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

Then, an end-user can make use of this function to create new types following the above explanation:

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. 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 (e.g., 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)?

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?


回答1:


Given Scala is one of the tagged languages, here is a solution in Dotty (aka. Scala 3). Take this with a grain of salt, since Dotty is still under development. Tested with Dotty version 0.24.0-RC1, here is a Scastie that proves this actually compiles.

Scala doesn't have the same sort of built-in type machinery as TypeScript for manipulating records. 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 a match-type. The never type in TypeScript is called Nothing in Scala/Dotty.

// 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:

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



回答2:


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).

Yes, dynamic types are fully supported by the OCaml/ReasonML type system and are widely used. You can express quite complex dynamic typing rules, e.g., 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. 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. 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.

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,

type 'a witness = ..

To create a new witness (basically incrementing this id) we will use first class modules and append a new constructor using +=

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,

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

And this is how we implement the cast 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,

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 



回答3:


Disclaimer: I'm not a C++ programmer, so don't take this answer to be the proper way to do it in 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>>

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

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++.



来源:https://stackoverflow.com/questions/62248481/how-to-use-type-level-functions-to-create-static-types-dynamically

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!