New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow recursive typedef #3714
Comments
The reason that this is not allowed is that type aliases are not nominative, but structural. An alias like typedef LinkedList<T> = (T head, LinkedList<T>? tail); doesn't introduce a new type, only a name for an existing type. We don't know. This definition doesn't define a type, but a recursive constraint on a type. We have to define how to solve that equation. The obvious answer is to give the fixed point. The only problem is that that is an infinite type, and we need to represent it in finite memory, and time, so we can't just unfold it. The other option is to introduce fixed point operators in the type model. 𝕗𝕚𝕩 T = (T head, LinkedList<T>? tail); That is defined to be the (least?) fixed point of that equation. Which is an infinite, but regularly recurring, type. We have to derive that type from a declaration like: typedef F<X, Y> = Map<X, F<String, List<Y>>>; by ... probably being clever and seeing which parameters occur recursively. typedef F<X, Y> = Map<X, 𝕗𝕚𝕩 $F<Y> = Map<String, List<$F<List<Y>>>>>; Maybe. typedef B = List<C>;
typedef C = List<B>; and equivalent to typedef L = List<L>; ? Sure. But how easy is it do detect it?) The defintion would have to not be directly recursive. A Then we need to define what every function we have on types means for this type: subtyping, normalization, least upper bound, and assig an interface to the type (which the "no direct recursion" helps with, since that will likely give an outermost type). And it's a real type, which can exist at runtime as the reified your argument of, say, a list, so we need to do subtyping tests at runtime too. And we have to figure out what it means if someone combines more than one of those with each other. That's a possible approach. It might be undecidable. I'm not enough of a type theorist to know, but it looks too much like lambda calculus to not make me fear that it's Turing complete. |
My case: typedef State = State? Function(Parser parser);
State state = fragment;
while (index < length) {
state = state(parser) ?? fragment;
} |
The recursive types that actually work are usually the ones including functions, nullability or collections. That's all examples of not having to actually produce a value of a component type of the recursive type, so the recursion can end for an actual value. Saying Saying So, there are some uses of recursive types that make sense. You can have your recursive type today, if you're willing to put a class in the middle. class State {
final _State? Function(Parser) _callback;
State(this._callback);
State? call(Parser parser) => _callback(parser);
}
State fragment = State(_fragmentCallback);
// ...
State state = fragment;
while (index < length) {
state = state(parser) ?? fragment;
} Nominative types (class types, really, because extension types have problems with self-references too) is the officially recommended way to have self-referential types. It works because the compiler doesn't need to unfold the recursion until it reaches an object of that type, and the type system ensures that the value will satisfy the type. The object itself knows its precise type (instance of that class, type arguments are this and that), which tells everything that is needed about the type. There is no need to check component values, it's entirely a type comparison operation. For structural types, there is no inherent type to compare against. Well, not for records at least, functions do have an inherent function type. And union types have as much type as the actual value. So it's records. Recursive structural types are not necessarily impossible. Heck, the Trine language (the programming language used in CS 101 when I studied) had recusive structural typing, and was able to recognize that Subtyping of recursive types is well studied. (We did manage to allow |
I was curious if there were any other languages that support a recursive typedef like in this proposal. I found that most languages which support a typedef do not support exactly these recursive definitions (although in some cases the language may have some alternative solution). Examples that don't support recursive typedefScala// illegal cyclic type reference: alias ... of type LinkedList refers back to the type itself
type LinkedList[T] = (T, Option[LinkedList[T]])
// illegal cyclic type reference: alias ... of type Church refers back to the type itself
type Church = Church => Church Kotlin// Recursive type alias in expansion: LinkedList
typealias LinkedList<T> = Pair<T, LinkedList<T>?>
// Recursive type alias in expansion: Church
typealias Church = (Church) -> Church Haskell-- Cycle in type synonym declarations:
-- Main.hs:8:1-45: type LinkedList a = (a, Maybe (LinkedList a))
type LinkedList a = (a, Maybe (LinkedList a))
-- Cycle in type synonym declarations:
-- Main.hs:7:1-30: type Church = Church -> Church
type Church = Church -> Church Swift// type alias 'LinkedList' references itself
typealias LinkedList<T> = (T, LinkedList<T>?)
// type alias 'Church' references itself
typealias Church = (Church) -> Church Go// generic type cannot be alias
// invalid recursive type: LinkedList refers to itself
type LinkedList[T interface{}] = struct {
head T
tail *LinkedList[T]
}
// invalid recursive type: Church refers to itself
type Church = func(Church) Church F#// This type definition involves an immediate cyclic reference through an abbreviation
type LinkedList<'t> = 't * Option<LinkedList<'t>>
// This type definition involves an immediate cyclic reference through an abbreviation
type Church = Church -> Church Rust// error[E0391]: cycle detected when expanding type alias `LinkedList`
type LinkedList<T> = (T, Option<LinkedList<T>>);
// error[E0391]: cycle detected when expanding type alias `Church`
type Church = fn(Church) -> Church; C++// error: use of undeclared identifier 'LinkedList'
template<class T>
using LinkedList = std::tuple<T, LinkedList<T>>;
// error: unknown type name 'Church'
using Church = Church(*)(Church); OCaml(* Error: The type abbreviation linkedlist is cyclic *)
type 'a linkedlist = 'a * 'a linkedlist option;;
(* Error: The type abbreviation church is cyclic *)
type church = church -> church;; Elm-- This type alias is recursive, forming an infinite type!
type alias LinkedList a = (a, Maybe (LinkedList a))
-- This type alias is recursive, forming an infinite type!
type alias Church = Church -> Church Examples that do support recursive typedefTypescriptHowever, I found that Typescript DOES allow exactly this sort of recursive typedef. The following is a valid Typescript program: type LinkedList<T> = [T, LinkedList<T>?];
type Church = (c: Church) => Church; The following definitions are also allowed in Typescript: type TurtlesAllTheWayDown = Array<TurtlesAllTheWayDown>;
type Void = {value: Void}; However this definition is not: type Void = Void; // Type alias 'Void' circularly references itself. HaxeThe Haxe language appears to also support the same rules as Typescript for it's typedef feature. typedef LinkedList<T> = {head: T, tail: Null<LinkedList<T>>};
typedef Church = Church -> Church; These definitions are also allowed: typedef TurtlesAllTheWayDown = Array<TurtlesAllTheWayDown>;
typedef Void_ = {value: Void_}; However this definition is not: typedef Void_ = Void_; // Recursive typedef is not allowed OdinThe following definitions are allowed in Odin. Although I will point out that the LinkedList :: struct($T: typeid) { head: T, tail: ^LinkedList(int) }
Church :: proc(Church) -> Church For TurtlesAllTheWayDown :: []TurtlesAllTheWayDown
turtles : TurtlesAllTheWayDown = {} // Segmentation fault (core dumped) (exit status 139) The following is allowed: Void :: struct {value: ^Void} But these definitions are not: Void :: struct {value: Void} // Illegal declaration cycle of `Void`
Void :: Void // Invalid declaration value 'Void' PuppetThis language is not free or open source, so I didn't actually try this one out, but it's clear from the documentation that recursive typedef is allowed in this language: https://www.puppet.com/docs/puppet/5.5/lang_type_aliases.html#creating-recursive-types Given that there does seem to be a handful of languages that support this feature, it seems plausible that it may at least be technically possible for dart to support it. If anyone is interested in experimenting with recursive typedef I think Haxe and Typescript are probably the best languages to try. |
Typescript is probably being clever enough to recognize that the recursion goes through a nullable type or a function type. The reason that matters is that you can create a value of the recursive type without already having a value of the type. (Or it's being naive enough to not recognize that there is a problem, it just never tries to unfold the type until it needs to. It's easier in a language without type soundness.) type LinkedList<T> = [T, LinkedList<T>?]; // [1, null] is a LinkedList<int>
type Church = (c: Church) => Church; // function(_) { throw "Nope"; } is a Church
type TurtlesAllTheWayDown = Array<TurtlesAllTheWayDown>; // [] is a TurtlesAllTheWayDown
type Void = {value: Void}; // {get value() { throw "nope"; }} is a Void A type like Doing The distinction between nominal type and structural type can also be seen in Haskell. |
Apologies if this is a duplicate, but I don't think there is an existing issue specifically on this topic.
Occasionally I run into situations where it would be nice if a
typedef
could refer to itself by name in its definition.Often this takes the form of a simple data structure like one of the following:
The alternative is to write a class which is significantly more verbose:
Another example, I came across this Rosetta Code problem called Church numerals.
I attempted to translate the java solution to dart as follows:
The above solution works, but is not particularly type safe. It would be better if we could define
ChurchNum
as follows:You can of course implement a type safe solution by defining
ChurchNum
as a class that accepts aChurchNum Function(ChurchNum)
in the constructor, but then you have unnecessary wrapper objects, and a more verbose solution.A third situation where this could be useful is if union types were also added to the language.
With both features you could define a
Json
for example:That said, I think this feature would be useful even on its own.
The text was updated successfully, but these errors were encountered: