Skip to content
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

Closed
rpominov opened this issue May 22, 2015 · 25 comments
Closed

Support nominal type aliases #465

rpominov opened this issue May 22, 2015 · 25 comments

Comments

@rpominov
Copy link

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:

/* @flow */

type UserId = number;
type NumberOfFollowers = number;

function hasLotOfFollowers(followers: NumberOfFollowers): boolean {
  return followers > 1000;
}

var userId: UserId = 100;

// Expecting error here
hasLotOfFollowers(userId);
$ flow
No errors!
@samwgoldman
Copy link
Member

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:

type UserId = $Nominal<number>

How would you create an inhabitant of the UserId type? Would number be able to flow into a UserId?

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);
test.js|23 col 26 error|  AdminID
|| This type is incompatible with
test.js|3 col 7 error|  UserID
|| 
test.js|24 col 28 error|  UserID
|| This type is incompatible with
test.js|13 col 7 error|  AdminID

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?

@rpominov
Copy link
Author

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!

@avikchaudhuri
Copy link
Contributor

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

newtype userid = number // or type userid = $Nominal<number> as you have above

that satisfies:

var u: userid = userid(0);
(u: number); // OK, we can explicitly require unwrapping to be stricter, but this should suffice

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?

@samwgoldman
Copy link
Member

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?

newtype userid = number; // agree this is good "official" syntax, but I would start with $Nominal
var u = (0 : userid);
u * 10; // error
(u : number) * 10; // ok

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 samwgoldman changed the title Type aliases are just _aliases_, not separate types Support nominal type aliases Jun 24, 2015
@gregwebs
Copy link

gregwebs commented Sep 9, 2015

@samwgoldman allowing casting for this anywhere is problematic

  • you are now taking any newtype of a number in your multiplication function
  • there is no restriction on where wrapping and unwrapping can occur

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 un prefix, so unUserId.

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 unUserId.
Now by not exporting the newtype constructor I can guarantee that the rest of my functions are properly working with minutes.

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 (0 : number -> userid) and (u : userid -> number). Note that I am not using the fat arrow which is already used for function types.

@samwgoldman
Copy link
Member

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

@raould
Copy link

raould commented Nov 4, 2015

+1 to this overall request. This is one of the first things I look for in a type system.

@raould
Copy link

raould commented Nov 9, 2015

To contribute to the bike shed UX discussion of the arrow, I propose ">". Or you can consider avoiding an arrow: just ""; or "castto"; or "x from y"; or reversing the order so the arrow points backwards to be really even more distinguished from "=>", are other brainstorms. :-)

@latos
Copy link

latos commented Jul 29, 2016

You can achieve the desired effect with this trick (not sure if this is mentioned elsewhere?):

// @flow


type UserId = { _Type_UserId_: void }  // dummy field unlikely to happen accidentally in code


function UNSAFE_CAST(x:any) { return x; }
var NewUserId : (val:string)=>UserId = UNSAFE_CAST;
var FromUserId : (userId:UserId)=>string = UNSAFE_CAST;


var userId : UserId = NewUserId('u1');

// At runtime, just a naked string
console.log("Runtime value:", userId, "Runtime type:", typeof userId);

// This line would fail the flow type check
// var str : string = userId;

var str : string = FromUserId(userId);

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.

var userId2 : UserId = ('u1':any);
var str : string = (userId:any);

The second line could be made a little less distasteful

// assert that it was a UserId before unsafely casting it to string
var str : string = ((userId:UserId):any);

@vkurchatkin
Copy link
Contributor

@latos using classes is better for this purpose, because they are actually nominally typed

@gcanti
Copy link

gcanti commented Aug 4, 2016

@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)
}

@latos
Copy link

latos commented Aug 4, 2016

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

@vkurchatkin
Copy link
Contributor

@latos no, it doesn't. It's the same thing: you use any to cast primitive to a class type

@gcanti
Copy link

gcanti commented Aug 4, 2016

@latos with unwrap I mean that you must do something (a cast or pass through FromUserId) in order to get a string, otherwise while the runtime type is the raw primitive, Flow will still complain. Example:

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 UserId value in your code. I agree that there's no runtime overhead but seems pretty awkward. On the other hand using classes adds a small runtime penalty but it's way more clean and future proof.

going hacky IMO refinements capture better the spirit of the original question of @rpominov (i.e. UserId !== NumberOfFollowers as types), plus they don't add the requirement to always unwrap in order to use them

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, UserId and NumberOfFollowers are still numbers, but I think is a feature in this context.

@latos
Copy link

latos commented Aug 5, 2016

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

  • a physical unit, e.g. "distance" that might be internally represented in metres, but you don't want it to accidentally get rendered directly. you always want it to be explicitly rendered to a string in the current "units" constant (e.g. metric or imperial). In an engineering tool, missing a spot & printing the wrong units could be catastrophic
  • a "password" or "credit card" details value that you don't want to be logged or output accidentally, you would normally convert it to a string via a sanitising helper
  • similarly, an "unsafe raw html" type, etc.
    Of course, in the above examples, not all may necessarily need the "zero cost abstraction" - you could just actually box them at runtime.

@gcanti
Copy link

gcanti commented Aug 20, 2016

@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)"

@freddi301
Copy link

It should be added to the documentation until it is not imlemented.
This https://medium.com/@gcanti/phantom-types-with-flow-828aff73232b#.5w33oy8hc is very similar.
These compile time only refinements are very useful in a lot o ways eg. domain-driven-design
number and string refinements, and class/object meta-annotation (like empty interfaces in java)

@praxiq
Copy link

praxiq commented Nov 15, 2016

Instead of $Nominal, might it be appropriate to use the existing $Subtype for this?

going back to the original example:

type UserId = $Subtype<number>;
type NumberOfFollowers = $Subtype<number>;

As far as I can tell, this currently does nothing (it's the same as making them both type number). However - maybe I'm misunderstanding what is meant by subtype, but it seems to me this is supposed to say: every UserId is a number, and every NumberOfFollowers is a number, but an unqualified number cannot be assumed to be one of those.

Without adding new syntax, these types could be used with the foo: UserID = (3: any) trick.

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 foo = (3: !UserID) as shorthand for the "upcast to any, then downcast to the specified type."

@mkscrg
Copy link

mkscrg commented Dec 21, 2016

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

@damncabbage
Copy link

damncabbage commented Jan 3, 2017

@mkscrg: Nice!

I had a go at shrinking this a little (removing the redundant type / Outer parameter) by using the this 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 Foo.to(Foo.to(42)), Foo.from(Bar.to(42)) and other nonsense through. It'll "work", but allow things through it shouldn't.)

Follow-up edit: I've been reticent to use the "Newtype" terminology, mostly because newtype lets you do things that we can't with this, eg. newtype Foo a = Foo (a -> String), where it's not a straight "wrapping". I'm using the term TypeWrapper in my own code.

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.

@raould
Copy link

raould commented Jan 10, 2017

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?

@damncabbage
Copy link

damncabbage commented Jan 10, 2017

@raould:
When the Flow types are stripped out, you're still left with two things:

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

@grassator
Copy link

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

@damncabbage
Copy link

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

@jbrown215
Copy link
Contributor

Closing since opaque types went out in 0.51
docs here: https://flow.org/en/docs/types/opaque-types/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests