-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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
Suggestion: sum types / tagged union #186
Comments
👍 |
+Suggestion, Needs Proposal |
I would love such feature. enum Color {
Red,
Green,
Blue,
Rgb(r: number, g: number, b: number)
}
function logColor(color: Color) {
switch(color) {
case Color.RGB(r, g, b):
console.log('rgb(' + r + ', ' + g + ', ' + b + ')');
break
default:
console.log(Color[color])
break;
}
}
var myColor: Color = Color.Rgb(255, 0, 255);
var red: Color = Color.Red;
logColor(myColor); //should ouput 'rgb(255, 0, 255)'
logColor(red); //should ouput 'Red'
console.log(Color[myColor]); // should output 'Rgb'
console.log(Color[3]); // should output 'Rgb';
console.log(Color[Color.Rgb]); // should ouput 'Rgb'
console.log(Color['Rgb']); // should ouput '3'
I tried to hack a little something, it seems to work but i'm not sure if it worth the outputted JS var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
Color.Rgb = function (r, g, b) {
var result = [r, g, b];
result.valueOf = result.toString = function () { return 3; };
return result;
};
Color.Rgb.toString = Color.Rgb.valueOf = function () { return 3; };
Color[3] = "Rgb";
})(Color || (Color = {}));
function logColor(color) {
switch(+color) {
case 3:
var r = color[0];
var g = color[1];
var b = color[2];
console.log('rgb(' + r + ', ' + g + ', ' + b + ')');
break;
default:
console.log(Color[color])
break;
}
}
var myColor = Color.Rgb(255, 0, 255);
var red = Color.Red;
logColor(myColor); //should ouput 'rgb(255, 0, 255)'
logColor(red); //should ouput 'Red'
console.log(Color[myColor]); // should output 'Rgb'
console.log(Color[3]); // should output 'Rgb';
console.log(Color[Color.Rgb]); // should ouput 'Rgb'
console.log(Color['Rgb']); // should ouput '3' note the |
I suggest an implementation that leverages the existing lookup power of objects (not using switch/case but instead using parameter names). Here's what I'm thinking of... [Note: I've changed the example to avoid using colour as that's a bit confusing as colours are made of RGB. Example now uses Animals.]
That would approximately correspond to a JS object that is assumed to have exactly one of the following parameters, i.e. be treated a bit like:
When you define a variable of this type, e.g.
it can be compiled like so:
Then you can match in a more standard functional programming style of syntax like so:
Which would be compiled to JS like so:
That seems to provide reasonable balance of conciseness, readablity and efficiency. |
Can we close this as a duplicate of #14? |
@RyanCavanaugh for me it's something quite different than union type more an extension of enum. |
Maybe worth thinking of this more like an algebraic data type? |
I second that. Unions and discriminated unions are very different both in implementation and semantics. Plus, non-discriminated unions are already used a lot in actual JavaScript code out there, which gives them a much higher priority. |
A major concern I have is how to effectively use types like this without adding a lot of expression level syntax for operating over them. How useful will it be if there's no 'match' type operator to nicely decompose these in a type safe manner? |
Union types and sum types (i.e. disjoint tagged unions) are really different concepts, with different use cases. Union types are certainly very important to represent faithfully the API of existing Javascript libraries, although a full support for them is tricky since you cannot in general determine type-information based on runtime values. Sum types are more important, I'd say, to write high-level programs, not necessarily related to existing Javascript code bases, particularly for everything which pertains to symbolic processing. Having them in the language would make it a great choice for a whole new class of problems. @danquirk Extending the existing switch statement to allow capturing "enum arguments" would be a natural first step. See @fdecampredon's first example. This would not introduce a lot of new expression level syntax. |
Another possible view of sum types in TypeScript would be as an extension of regular interfaces with an "exclusive-or" modality instead of an "and". interface MySumType { This would classify objects that contains exactly one of the mentioned fields (with the corresponding type). This might be closer to how people would naturally encode sum types in Javascript. |
@alainfrisch 👍 that's the way I was coding it up in my suggestion above :) |
👍 |
Note that union and sum are two different operations: Union is the coproduct in the category of sets and inclusions. Sum is what's used in algebraic data types. In Haskell, the pipe | does not mean union, it means sum: data twoString = L string | R string |
@danquirk, do you think the following implementation is too heavy as far as expression syntax?
generated javascript
|
or something like this which I like better
generated javascript
|
TypeScript already has some support for ADTs using standard class inheritance. I propose a mild desugaring: data Tree<A> {
Node(a: A, left: Tree<A>, right: Tree <A>);
Leaf();
}
var myTree = Node(Leaf(), Leaf());
match (myTree) {
Node(v, l, r): foo(l);
Leaf(): bar;
} should desugar to the following TypeScript: interface Tree<A> {}
class Node<A> implements Tree<A> {
constructor (public a: A, public left: Tree<A>, public right: Tree<A>) {}
}
class Leaf<A> implements Tree<A> {
constructor () {}
}
var myTree = Node(Leaf(), Leaf());
switch (myTree.constructor.name) {
case 'Node': ((v,l,r) => foo(l)) (myTree.a, myTree.left, myTree.right); break;
case 'Leaf': (() => bar) (); break;
} In the code above, I've used the (myTree instanceof Node) ? ((v,l,r) => foo(l)) (myTree.a, myTree.left, myTree.right)
: (myTree instanceof Leaf) ? (() => bar) ()
: void 0; |
@Aleksey-Bykov as much as I love the syntax used for this concept in functional languages I think our current type guard concept is a very elegant solution for the problem in TypeScript. It allows us to infer and narrow union cases using existing JavaScript patterns which means more people will benefit from this than if we required people to write new TypeScript specific syntax. Hopefully we can extend type guards further (there are a number of suggestions for that already). |
The bigest benefit of sum types is their propery of exhaustiveness or
|
That's definitely a big part of their value. It's something I would like to look at but haven't filed a formal issue for yet. Certainly you could imagine an error for something like this: function foo(x: string|number) {
if(typeof x === "string") { ... }
// error, unhandled case: typeof x === "number"
} the question is whether it could be robust enough to handle more complicated cases. |
Another thing I truly dont understand about type guards is when I do a
How is this supposed to work?
|
Well that wouldn't do what you want in JavaScript either if x was some object literal. Your type guard would either be a set of property checks if(x.length) { ... }
if(x.aPropertyInA) { ... } or we could do something like #1007 |
Is checking for property considered a type guard? I mean I know I can sniff
|
Not at the moment, but I implemented it over here to test it out #1260. So you can see why I might think there's enough room to extend this functionality far enough to get the full breadth of checking you're imagining without needing new syntax (which means more people benefit and we make life easier on ourselves if that syntax is needed by ES later). |
I am all up for keeping as less new syntax as possible. If you can fit a
|
You could set the prototype's The question is if TypeScript will allow that too, or you get an error (with/without classes) |
It would be nice to have a That way you could do this:
|
@andy-hanson You can do that with a union type: type Super = Sub1 | Sub2;
class Sub1 { a: string; }
class Sub2 { b: string; }
function (s: Super) {
if (s instanceof Sub1) {
// s: Sub1
} else {
// s: Sub2
}
} |
@ivogabe The idea is that one should be able to define the class (with methods) and the type as the same thing, rather than having separate |
@andy-hanson Can't you just override required methods on |
If I'm not mistaken, ScalaJs has to handle a similar concept. abstract class Exp
case class Fun(e: Exp) extends Exp
case class Number(n: Int) extends Exp
case class Sum(exp1: Exp, exp2: Exp) extends Exp
case class Product(exp1: Exp, exp2: Exp) extends Exp
def print(e: Exp): String = e match {
case Number(1) => "1"
case Number(x) => x.toString
case Sum(Number(1), Number(2)) => "(1 + 2)"
case Sum(e1, e2) => "(+ " + print(e1) + " " + print(e2) + ")"
case Product(e1, e2) => "(* " + print(e1) + " " + print(e2) + ")"
case Fun(e) => "(fn [] " + print(e) + ")"
} compiles to $c_Ltutorial_webapp_TutorialApp$.prototype.print__Ltutorial_webapp_TutorialApp$Exp__T = (function(e) {
var rc19 = false;
var x2 = null;
var rc20 = false;
var x5 = null;
if ($is_Ltutorial_webapp_TutorialApp$Number(e)) {
rc19 = true;
x2 = $as_Ltutorial_webapp_TutorialApp$Number(e);
var p3 = x2.n$2;
if ((p3 === 1)) {
return "1"
}
};
if (rc19) {
var x = x2.n$2;
return ("" + x)
};
if ($is_Ltutorial_webapp_TutorialApp$Sum(e)) {
rc20 = true;
x5 = $as_Ltutorial_webapp_TutorialApp$Sum(e);
var p6 = x5.exp1$2;
var p7 = x5.exp2$2;
if ($is_Ltutorial_webapp_TutorialApp$Number(p6)) {
var x8 = $as_Ltutorial_webapp_TutorialApp$Number(p6);
var p9 = x8.n$2;
if ((p9 === 1)) {
if ($is_Ltutorial_webapp_TutorialApp$Number(p7)) {
var x10 = $as_Ltutorial_webapp_TutorialApp$Number(p7);
var p11 = x10.n$2;
if ((p11 === 2)) {
return "(1 + 2)"
}
}
}
}
};
if (rc20) {
var e1 = x5.exp1$2;
var e2 = x5.exp2$2;
return (((("(+ " + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e1)) + " ") + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e2)) + ")")
};
if ($is_Ltutorial_webapp_TutorialApp$Product(e)) {
var x13 = $as_Ltutorial_webapp_TutorialApp$Product(e);
var e1$2 = x13.exp1$2;
var e2$2 = x13.exp2$2;
return (((("(* " + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e1$2)) + " ") + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e2$2)) + ")")
};
if ($is_Ltutorial_webapp_TutorialApp$Fun(e)) {
var x14 = $as_Ltutorial_webapp_TutorialApp$Fun(e);
var e$2 = x14.e$2;
return (("(fn [] " + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e$2)) + ")")
};
throw new $c_s_MatchError().init___O(e)
});
// type check for Number class
function $is_Ltutorial_webapp_TutorialApp$Number(obj) {
return (!(!((obj && obj.$classData) && obj.$classData.ancestors.Ltutorial_webapp_TutorialApp$Number)))
} |
@roganov: Many people prefer a style of programming where functions are defined only once, as opposed to once per subclass. That's why this issue here exists. |
Since we have string literal types, I would propose to interpret the following declaration interface Action[type] { // the name "type" will be used as discriminator tag
"REQUEST": {}
"SUCCESS": { data: string }
"FAILURE": { error: any }
} as fully equivalent shorthand to type Action = { type: "REQUEST" }
| { type: "SUCCESS", data: string }
| { type: "FAILURE", error: any } and detect such situation in if all conditions are met:
then in the proper following context the type of Thus, we will be able to write var a: Action;
// ...
if (a.type === "SUCCESS") {
console.log(a.data) // type of a is narrowed to {type: "SUCCESS", data: string}
} or even function reducer(s: MyImmutableState, a: Action): MyImmutableState {
switch (a.type) {
case "REQUEST": // narrow a: { type: "REQUEST" }
return s.merge({spinner: true});
case "SUCCESS": // narrow a: { type: "SUCCESS", data: string }
return s.merge({spinner: false, data: a.data, error: null});
case "FAILURE": // narrow a: { type: "FAILURE", error: any }
return s.merge({spinner: false, data: null, error: a.error});
default: // widen a: { type: string }
return s;
}
} This proposal is less powerfull than full algebraic types (it lacks recursion), nevertheless it's rather pragmatic as it helps to assign the correct types to commonly used patterns in JavaScript especially for React + (Flux/Redux). I know that narrowing is working only on simple variables, but I think this case is somewhat ideologically equivalent to the type-checking for expressions assigned to the simple variable. @danquirk, what is your opinion? |
@Artazor I've already done some investigation into equality -based type narrowing - It is pretty cool to use. |
@Artazor with @weswigham's type narrowing branch, and some recent changes I've made, something like that works. It needs to happen one step at a time though. |
If you're looking for matching sugar for using existing types, maybe allow destructuring in cases?:
This may already have a (nonsense) meaning of check if
Alternatively: Perhaps depend on adding C++-style condition declarations (
I think this is closer to how languages with pattern matching work in a way that feels like where ES is going, but it seems like (something like) it should be proposed as an ES feature first. |
FWIW @Artazor's proposal #186 (comment) is what flow does (disjoint unions) : http://flowtype.org/docs/disjoint-unions.html#_ type Action = { type: "REQUEST" }
| { type: "SUCCESS", data: string }
| { type: "FAILURE", error: any } Conditional checks on |
@basarat I have a version of our narrowing code which allows this behavior On Tue, Mar 15, 2016, 7:44 PM Basarat Ali Syed notifications@github.com
|
Implementation of discriminated union types using string literal type tags is now available in #9163. |
Use an
That is a structural guard which although may be a useful feature but may not always be applicable, thus my suggestion above to use
Which appears to me to be undesirable.
Which is a nominal sum type (aka ADT) and employs nominal guards.
Without the So However, if ever nominal typeclasses were supported (a la Rust or Haskell), then the entire point of typeclasses (versus class subtyping) is to not conflate the definition and implementations of new interfaces on data type in order to have more degrees-of-freedom in compile-time extensibility. Thus, the relationship between interface structures becomes nominal and orthogonal to the definition of the data type or nominal sum type, and the extra declaration of
That is structural (not nominal) sum typing.
And it isn't compatible with nominal typeclasses; thus conflates interface (declaration and implemention) with data type, i.e. the interfaces (e.g. |
A very nice addition to TypeScript's type system would be sum types in the spirit of ML-like languages. This is one of basic and simple programming constructs from functional programming which you really miss once you get used to it, but which seem to have a hard time being included in new modern languages (contrary to other features from functional programming such as first-class functions, structural types, generics).
I guess the most natural way to integrate sum types in the current language syntax would be to extend enum variants with extra parameters (similarly to what Rust does: http://doc.rust-lang.org/tutorial.html#enums ) and upgrade the switch statement to a more powerful structural pattern matching (although in a first step, simply discriminating on the toplevel variant and capturing its parameters would be already quite good).
This is quite different from other the proposal about "union types" (#14), which would mostly be useful to capture types in existing Javascript APIs. Sum types are rather used to describe algebraic data structures. They would be particularly useful for any kind of symbolic processing (including for implementing the TypeScript compiler).
The text was updated successfully, but these errors were encountered: