-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Support nominal type aliases #465
Comments
Flow's type system is structural by design and what you are proposing would behave more like a nominal type system. I don't believe the existing behavior of type aliases will change, but we might consider supporting a way to make nominal types, because this absolutely would provide type safety. However, there are a few details to sort out. Assume we had something like the following syntax:
How would you create an inhabitant of the I am reminded of Haskell's newtype wrappers. You need to "construct" the value in the newtype and "unwrap" the newtype to get at the inner value, but the compiler is able to erase that at compile time, making it a zero-cost abstraction. Ignoring the compiler support for unwrapping for a moment, we currently support something like this: /* @flow */
class UserID {
id: number;
constructor(id: number) {
this.id = id;
}
get() {
return this.id;
}
}
class AdminID {
id: number;
constructor(id: number) {
this.id = id;
}
get() {
return this.id
}
}
var userID: UserID = new AdminID(1);
var adminID: AdminID = new UserID(2);
That is to say, classes are nominal in Flow. I think it would be VERY interesting to support a newtype semantics for classes like this, which wrap a single value, but that would involve rewriting javascript, which flow doesn't actually do—babel does it for us. Ultimately, I don't have an answer here. Can you write your code to use a class wrapper for these values? |
Thank you for the detailed answer! It would be indeed very interesting if Flow would support something like nominal types. This would provide support for extra type safety for those, who want it, but I understand now that the cost will be to annotate every creation of an inhabitant. I was considering using classes as wrappers, but in my particular case it would be an overkill. Anyway this technique might be useful in some cases. Thanks! |
Yeah, thanks @samwgoldman for the detailed answer. We have thought about newtype off and on, and don't quite know how to do it other than inventing wrapper syntax that gets stripped off. So something like
that satisfies:
Basically this design allows userid to be used wherever numbers can, e.g. in arithmetic operations. But not all numbers are userids. Also, not all userids are ages, if age is another newtype: the wrapping has to be explicit. Want to work on something like this? |
Stripping off whatever wrapper syntax is the big hurdle here, IMHO. Adding functionality to flow that transforms source files makes me hesitate to volunteer for this one. Maybe we could leverage the casting syntax both ways?
Then, instead of using the vanilla flow logic for type casts, run something with special cases for type aliases before falling back to vanilla flow. |
@samwgoldman allowing casting for this anywhere is problematic
In Haskell one controls all this at the definition site, in particular whether a constructor is exported and/or a function for unwrapping is exported. By convention the unwrapping function usually has an newtype UserId = UserId { unUserId :: Int } deriving (Read, Show, Eq, Ord, Num) If I only want to provide the ability to unwrap, but not to construct, I just export So to use casting as is, one needs the ability to do fine-grained exports. However, the other problem is the ambiguity of over-loading casting. It is no longer clear whether the user wants to cast or deal with a newtype. So you might consider adding a new syntax such as |
@gregwebs Good insight. Our constraint is that we can only play with the type language, not the runtime language. Seems like you're keyed into that, based on the syntax you recommended. I think that syntax could work (although I worry about confusion when a user types -> isntead of =>, as I sometimes do). Regardless, your point about newtypes being one-to-one is well taken. |
+1 to this overall request. This is one of the first things I look for in a type system. |
To contribute to the bike shed UX discussion of the arrow, I propose " |
You can achieve the desired effect with this trick (not sure if this is mentioned elsewhere?):
This is pretty close to a zero-cost abstraction, except for calls to the no-op identity functions for "boxing" and "unboxing". However, one option here (if using something like webpack) could be to write a loader to strip out these conversion functions. Or one could just use the any type to box/unbox for free... e.g.
The second line could be made a little less distasteful
|
@latos using classes is better for this purpose, because they are actually nominally typed |
@latos I agree with @vkurchatkin since you must wrap / unwrap anyway. Your trick might be used to define custom refinements though type Integer = number & { __refinement_Integer: void };
function integer(n: number): ?Integer {
return n % 1 === 0 ? ((n: any): Integer) : null
}
function foo(n: number) {
return n.toFixed(2)
}
function bar(n: Integer) {
return n.toFixed(2) // <= no need to unwrap, `Integer`s are `number`s
}
integer(1.1) // => null
const i = integer(1)
foo(1)
// bar(1) // error
if (typeof i === 'number') {
foo(i) // <= again, `Integer`s are `number`s
bar(i)
} |
@vkurchatkin the aim of the exercise is to implement nominal types with zero overhead. Using a class has the overhead of boxing the primitive in an object, which is what this trick avoids. @gcanti the refined type thing you did is pretty cool. Not sure what you mean by "must we wrap/unwrap anyway"? the runtime type is the raw primitive. The NewUserId/FromUserId functions would probably get optimised out because they do nothing... but they are optional anyway, as you can just use the type casts instead as shown later in my example. It works the same as your example (no need to unwrap at runtime), except it adds the compile time requirement to wrap/unwrap, for stronger type safety, which I think is what @rpominov was after. E.g. a "UserId" type that is entirely incompatible with "string" at compile time in either direction. A refined type is incompatible in one direction only, which is also useful for some contexts (e.g. when we think of "integer" as a subtype of "number"). |
@latos no, it doesn't. It's the same thing: you use |
@latos with unwrap I mean that you must do something (a cast or pass through function foo(x: UserId) {
return x.length // <= here Flow complains: property `length`. Property not found in object type
} so you must unwrap function foo(x: UserId) {
return ((x: any): string).length
} and you must do that everytime you use a going hacky IMO refinements capture better the spirit of the original question of @rpominov (i.e. type UserId = number & { __refinement_UserId: void };
type NumberOfFollowers = number & { __refinement_NumberOfFollowers: void };
function userId(n: number): UserId {
// dummy refinement, predicate always true
return ((n: any): UserId) // <= wrap only once
}
function hasLotOfFollowers(followers: NumberOfFollowers): boolean {
return followers > 1000 // here I can use `>` without unwrapping
}
var userId = userId(100) // or even ((100: any): UserId)
hasLotOfFollowers(userId) // <= error! Yes, |
@vkurchatkin I think I see what you mean. You're saying, do the same trick, but use a class instead of the hack with the unlikely field? (I thought you meant to just use a class without any "unsafe" casting). I agree a class is a better idea :) @gcanti Yes, I consider the fact that you have to unwrap it to be a feature. Depending on context, it may or may not be what someone wants. For instance, if all I want is to declare I want a UserId, and no one can accidentally pass me a plain string (or a SomethingElseId) then doing the refinement trick is sufficient, and retains the convenience of using it as a string directly. However, I might also want the additional strength of having complete incompatibility with a string, because I don't want it accidentally concatenated/rendered in an inappropriate fashion. A few real-life examples would be:
|
@latos Good point. So the pattern would be: helper function unsafeCoerce<A, B>(a: A): B {
return ((a: any): B)
} newtype definition class Inches {}
const of: (a: number) => Inches = unsafeCoerce
const extract: (a: Inches) => number = unsafeCoerce newtype utilities function show(x: Inches): string {
return `Inches(${extract(x)})`
}
function lift(f: (a: number) => number): (a: Inches) => Inches {
return (a) => of(f(extract(a)))
}
function lift2(f: (a: number, b: number) => number): (a: Inches, b: Inches) => Inches {
return (a, b) => of(f(extract(a), extract(b)))
} Example: function log(n: number) {}
function sum(a, b) { return a + b }
const a = of(2)
const b = of(3)
const sumInches = lift2(sum)
log(a) // error: Inches. This type is incompatible with number
sumInches(1, 2) // error: number. This type is incompatible with Inches
show(sumInches(a, b)) // => "Inches(5)" |
It should be added to the documentation until it is not imlemented. |
Instead of going back to the original example:
As far as I can tell, this currently does nothing (it's the same as making them both type Without adding new syntax, these types could be used with the If there is new syntax added, I've always wished there were a succinct way to tell flow what I know about a type that it can't infer in a particular case. Something like |
That's a nice pattern @gcanti! Flow exposes type params to static class methods, so "newtype" can be a self-contained parent class: class Newtype<Outer, Inner> {
constructor: (_: { __DONT_CALL_ME__: void }) => void;
static to(x: Outer): Inner { return (x: any); }
static from(x: Inner): Outer { return (x: any); }
} class Foo extends Newtype<Foo, number> {}
const foo: Foo = Foo.from(42);
Math.abs(foo); // flow error
Math.abs(Foo.to(foo)) |
@mkscrg: Nice! I had a go at shrinking this a little (removing the redundant type / class Newtype<Inner> {
constructor(_: empty): void {}
static to(x: this): Inner { return (x: any); }
static from(x: Inner): this { return (x: any); }
} class Foo extends Newtype<number> {}
const foo: Foo = Foo.from(42);
Math.abs(foo); // Flow error
Math.abs(Foo.to(foo)) Demo here in the REPL. (Note: This only works from 0.37.0 onwards; previous versions allowed stuff like Follow-up edit: I've been reticent to use the "Newtype" terminology, mostly because Follow-up edit #2: I've put this up as a library here: https://www.npmjs.com/package/flow-classy-type-wrapper … with a blog-post explainer. |
generally since i'd like to use it for numbers for e.g. physics stuff, i'd like it to be as close to a zero runtime overhead cost abstraction as possible :-) Or is it such a small runtime overhead that I really shouldn't mind? @damncabbage thanks for the concise demo! I am guessing from the thread above that using 'class' still does have some runtime overhead? |
@raould:
To be clear, the "class" in this is just a handy container for defining the type, the "from" and the "to" functions all in one go. You're not creating new wrapped objects from this class; you're just calling the statically-defined functions as a way for Flow keep track of type conversions, with the tiny(?) run-time cost of passing in a value that is immediately returned. As a bonus: I've previously done some checking with V8 that saw it optimise away these no-op calls when they're called enough times (eg. in a hot loop), but I unfortunately don't have the test setup working anymore to post logs from. So yeah; I personally wouldn't worry about the overhead. Regardless, I can't think of any way of making this faster while preserving the (what I think are important) explicit to/from casting semantics that the above examples have. I hope that helps. 😄 |
@damncabbage Thank you for this nice solution—I've been diving into Haskell and really missed newtypes in Flow. After some playing around I realised that if you are willing to sacrifice the beauty of the code it's possible to completely avoid any overhead by using comment syntax: /* flow-include
class Newtype<Inner> {
constructor(_: empty): void {}
static to(x: this): Inner { return (x: any); }
static from(x: Inner): this { return (x: any); }
}
*/ /* flow-include
class Foo extends Newtype<number> {}
*/
const foo: Foo = /*::Foo.from(*/42/*::)*/;
Math.abs(foo); // Flow error
Math.abs(/*::Foo.to(*/foo/*::)*/) Besides being ugly, this also might not syntax highlight / intellisens nicely with the editors not supporting comment syntax, but other then that, this is true 0-overhead |
Opaque types landed in master a couple of days ago: 68dd89e 🎉 Sample use from https://twitter.com/vkurchatkin/status/886385324422836224 (from whom I found about this): |
Closing since opaque types went out in 0.51 |
Sometimes I have two types, and both of them are numbers, for instance, but at the same time have absolutely different meaning. Can I express this in Flow? I tried type aliases, but they are simply interpreted as the types they alias, so this code type checks:
The text was updated successfully, but these errors were encountered: