-
Notifications
You must be signed in to change notification settings - Fork 301
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
[WIP] Erase unions and records #2279
base: fable3
Are you sure you want to change the base?
Conversation
ncave
commented
Nov 17, 2020
- Erase unions and records to named tuples, behind a compiler switch.
- This is experimental and WIP.
Seems some test failing because this is overruling StringEnum or Erase attribute. Is that intended? I guess Erased and StringEnum unions should behave the same as without the optimization as they're used for JS interop. |
Right now we have three (potential) optimization flags, so it may be a good idea to tidy things up before the release of Fable 3 stablish. We have two options:
If we choose 2, we probably should do the following:
@ncave What do you think? |
@alfonsogarciacaro Kind of depends on the timeline for v3.0 release. Performance issues on Firefox can probably be dealt with by post-build down-compiling to ES5, if well documented. |
@alfonsogarciacaro Yes, this is still WIP and there is some fine-tuning to do, like Reflection and JS interop. |
You're right. I need to refrain myself from adding features at the last moment, even if there are behind a compiler flag 😅 Actually, it's been a few days without serious issues being reported so as soon as I'm done with a couple of blog posts we can a publish a "real" RC, and if there are no critical bugs reported we can republish again as stablish 👍 |
I would love to either write a standalone blog post on latest changes with respect and Fable 3 and a bunch libraries that are using the latest goodies:
I am thinking of pushing out Feliz v2.0 that uses many of the new stuff like units of measure support with witnesses, and deprecating the old |
That'd be great @Zaid-Ajaj! There's currently no way to get the compiler version from code right now, but we should add one. I was thinking in these two ways, we can implement one or both:
|
The compiler constant is just great! Really need that for a couple of stuff. As for the runtime value of the compiler, having maybe two properties would be even better: majorVersion : int
version : string |
2358f11
to
74a47b7
Compare
@ncave I just had an idea, what if convert the Record/Union/ValueType constructor into a singleton. The "erased" objects would contain a reference to their "type" so we can get the best of the two worlds: lightweight runtime types (basically arrays) but still we can identify the exact type and kind of an object at runtime. So something like this: type Foo = Foo of string | Bar of int * int
let b = Bar(3,4)
let name = Fable.Core.Reflection.getCaseName b Would compile to something like this: // The Union object contains default equality, etc, can be overridden.
const Foo = new Union("Foo", "Bar")
// Reflection info would be emitted as before
const b = [Foo, 1, 3, 4]
// This is a helper that returns the case name by accessing the Foo object
const name = getCaseName(b) What do you think? |
@alfonsogarciacaro Sure, perhaps that solves some corner cases, but would you elaborate on what would be the benefits vs. just a string for the case name in the array? Also, the case tag has to be first, so the comparison (order of cases) can work properly. |
I would have to run the tests with this PR as I don't remember exactly which tests are broken, but off the top of my head:
|
@ncave I'm thinking now the erasure should happen in the Fable2Babel step because the Fable transforms and the plugins should work on union expressions in the AST instead of checking all the possible shapes an erased union can take. |
@alfonsogarciacaro Doing it before the Fable AST phase makes this PR a lot smaller. But perhaps what you suggest makes sense, if we assume plugins want to know if something was a union or a record or a value type. I'm not entirely clear how the plugin flow works, what is the output of a plugin, is it Fable AST? Are they executed in a sequence? If so, then potentially they can have the same issue (one plugin destroying information by transforming the Fable AST before the other plugin sees it). |
Sorry @ncave, I should have given you more context. I thought of this when writing a plugin for Svelte integration. For example, this pattern match won't work if the union is erased, because we would lose the union type with the case names and fields. Likewise, if we want to match a new union expression, we would have to check for the erased form too which can quickly become very complicated.
Yes. Conceptually they are part of the Fable2Fable transforms. Right now there's only a plugin to transform member declarations and call expressions. For a technical reason, the call transformation is in the FSharp2Fable step but probably that's not a good idea and we have to change it at some point.
Yes, they will. And as you say, the day users apply several plugins to the same declaration/expression we're going to have a lot of fun. My hope is that most plugins won't be overlapping (they are only activate when the member is decorated with a specific attribute). |
In conclusion, my current thinking is unions (besides lists and options) should be represented as |
611657c
to
192f618
Compare
4bb9d33
to
33c7336
Compare
88e9c2e
to
9ad1b26
Compare
Sorry @ncave, I didn't check the current status of the PR but I had another "idea"💡 If we want to make the distinction into erased unions that simulate Typescript unions and erased unions for performance, we could have a compiler switch to erase structs (for performance) as this is what F# developers use when they want to make a union more performant. Although we have to make clear the semantics of a struct union with the compiler switch would still be different in Fable and .NET. About records, we should check what's the best way to erase them. I guess arrays would make smaller bundle sizes but JS objects would work better with hidden classes. |
@alfonsogarciacaro IMO using a compiler switch (like it is right now in the PR) is cleaner, as you can change the generated code output without making changes to the source code. The PR already makes exceptions for custom equality and a few other cases. These exceptions can perhaps be replaced later with static method invocations for specific types. Perhaps we can rename the flag from The current state of the PR is that it works, it's operational. Just a few test errors are left, 17 out of 1904, mostly having to do with JsInterop and toString/reflection. My definition of "operational" is that it's running the FCS-Fable benchmark, which has a bit of everything. The preliminary results are encouraging, even though not breathtaking. There is at least 5% performance improvement, and 10% bundle reduction (measured after treeshaking/minimization). That obviously depends on the code base usage of F# types. What is left to do is to move the transformation downstream to the Fable-to-JS phase, as you suggested, and fix the remaining tests, but it already works. About your other suggestion to replace the type name in the array with a constructor, it's worth trying but would it not impact the bundle size, as now all classes will be retained? Perhaps, I haven't tried. |
dcbdd1b
to
7ec3698
Compare
bdc58d3
to
131d71d
Compare
bd63a98
to
1895f4d
Compare
1527047
to
a69e20d
Compare
87e6a8c
to
b627427
Compare