-
Notifications
You must be signed in to change notification settings - Fork 21
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
Erased type-tagged anonymous union types #538
Comments
This is sound like it need to be implemented with CompilationRepresentationAttribute |
@alfonsogarciacaro erased unions are not only a thing for dynamic lang transpilation. They are also useful in message-based systems i.e. when you want to describe protocols in as a closed set of messages (in case of F# those could be discriminated unions). In that case a behavior that wants to satisfy more than one protocol, must have some way to define union of those, which so far is possible only as a lowest common denominator (usually an |
There are some other interesting reasons for this compiler feature. One is that we frequently hit situations in the F# compiler where a union type incurs an entire extra level in allocations, e.g. type NameResolutionItem =
| Value of ValRef
| UnionCase of UnionCaseRef
| Entity of EntityRef
| ... The needs for this type are relatively "low perf" (cost of discrimination doesn't really matter - multiple type switches are ok) but the type gets many, many long-lived allocations when the F# compiler is hosted in the IDE. One could make the type a struct wrapping an obj reference manually, but simply adding an annotation to represent this as an erased union type and discriminate by type switching would be a much less intrusive code change. (Note using a struct union would not work well as the struct would still have a dsicrimination tag integer, and would have one field for each union case - struct unions are by no means perfect representations for multi-case types as things stand at the moment)
:) There's not really any such thing as "S" for language features :) I'd say "M" or "L".
yes that would seem natural |
Will there be multiple |
@robkuz if the implementation will be with CompilationRepresentationAttribute then you can create your own erased union [<CompilationRepresentationAttribute(CompilationRepresentationFlags.ErasedUnion)>]
type DU<'a, 'b> = A of 'a | B of 'b |
@AviAvni @dsyme Please note that if this just enables a |
This is almost same as or on type operator.T1 or T2. In perspective can be realized by special attribute on function. Full erased from compiled code. But how to save metadata? |
I think the intent is that the types would be erased (like other F# information). The metadata would only available at compile-time through the extra blob of F#-specific metadata that F# uses |
I think that given the reasoning above (both for FABLE and the use case @Horusiath mentioned), this would be a good addition. 👍 |
Is it very important that the type is erased? Perhaps it's a slightly separate proposal, but I would love to have ad-hoc type unions in the form: let print (item : string | int) =
match item with
| s : string -> printfn "We have a string: %s" s
| i : int -> printfn "We have an int: %i" i Which would essentially compile down to the same IL as: let print (item : Choice<string, int>) =
match item with
| Choice1Of2 s -> printfn "We have a string: %s" s
| Choice2Of2 i -> printfn "We have an int: %i" i and, more importantly, at the callsite: print "Hello world" instead of: print (Choice1Of2 "Hello world") |
In Fable we've finally managed to remove the erased union case name by using the so-called erased/implicit cast operator let foo(arg: U2<string, int>) =
match arg with
| U2.Case1 s -> s.Length
| U2.Case2 i -> i
// No need to write foo(U2.Case1 "hola")
foo !^"hola"
foo !^5
// The argument is still type checked. This doesn't compile
foo !^5. |
TypeScript also supports string literals in these union types, i.e, in addition to Or alternatively, if string enums were supported like in TypeScript, we could acheive the same effect that way:
|
As a reference, Fable already supports string enums 😄 |
@Richiban is this what you're looking for? - Polymorphic Variants |
@Richiban @alfonsogarciacaro I hijacked this suggestion to convert this to a suggestion for erased ad-hoc type unions of the kind suggested by @Richiban (Note sure what the callsite would be though @Richiban - perhaps what you say)./ |
Can we make this types not erased? Why not introduce base implementation on Typed<'t1, 't2, ...> and make this as member of FSharp.Core |
@ijsgaus But if it's not erased then it's no difference from |
This seems like a really sweet suggestion! I imagine it could help the performance of a lot of library code. |
Would this help this problem? type Goose = Goose of int
type Cardinal = Cardinal of int
type Mallard = Mallard of int
type Bird = Goose | Cardinal | Mallard
let x = Goose 7 This code fails. Goose in the Bird DU shadows Goose as a type and turns it into an Atom. This shadowing happens silently and at least to me is surprising. type Goose = Goose of int
type Cardinal = Cardinal of int
type Mallard = Mallard of int
type Bird = Goose of Goose | Cardinal of Cardinal | Mallard of Mallard
let x = Goose 7 The type shadowing here still means I can't move forward, because there's no way to make a Goose.... type Goose = Goose of int
type Cardinal = Cardinal of int
type Mallard = Mallard of int
type Bird = Goose' of Goose | Cardinal' of Cardinal | Mallard' of Mallard
let x = Goose' (Goose 7) This works. This kind of situation happens where someone created a single case DU, and it gets consumed by someone who can't muck with the original DU for fear of breaking existing code. |
We wouldn't generate multiple C# overloads splitting out the choices - it's a technique fraught with problems. |
Can you elaborate? The only problem I can think of is a proliferation of overloads, but each overload would simply delegate to the actual erased-type overload, so hopefully wouldn't be too bloaty.
Exactly! This is a great feature for API modeling, which is why converting all these to
Note that this is already valid F# (albeit with an incomplete matches warning): let foo ("*"|"auto" as str) = printfn "Got %s" str |
Imagine the method being virtual, for example. Then multiple virtual slots are generated. Or imagine multiple untagged union paramaters generating an exponential number of overloads |
We are going to need guide users clearly about this. Giving clear use cases in online docs, to try to prevent an avalanche of terrible code that would result from users replacing DUs with this because this is shorter. And making sure no one uses this feature in assemblies which could be referenced by .Net languages other than F#. |
Not a big fan. Really seems like a super specific feature that should not be used 99.9% of the time. Looking at the code samples in the RFC and I strongly prefer the currently available solutions. It also looks like adding this will make anonymous DU's more unlikely.. Some things I thought while looking at the examples in the RFC
.. all of this can be archived with a DU right ? Also from looking at the exhaustivity checking it looks like there is still one case that can slip through - the base type not being explicitly handled. This also means that you actually need to know the base type. let prettyPrint (x: (int8|int16|int64|string)) =
match x with
| :? (int8|int16|int64) as y -> prettyPrintNumber y
| :? string as y -> prettyPrintNumber y
// common base type is object, value types get boxed.
let prettyPrint (x: obj) =
match x with
| :? int8 | :? int16 | :? int64 as y -> prettyPrintNumber y
| :? string as y -> prettyPrintNumber y
// unhandled cases - might be called from C#/VB
prettyPrint (null)
prettyPrint ([1..3] :> obj)
.. but are only really usable from F#.
.. as DU's do in a slightly different way. Still think what I described in #538 (comment) would enable the same use cases without adding the totally new concept of quite limited erased unions. Really don't want to deal with code like this (taken from the RFC samples) in the future. type Username = Username of string
type Password = Password of string
type UserOrPass = (Password | UserName) // UserOrPass is a type alias
// `getUserOrPass` is inferred to `unit -> UserOrPass`
let getUserOrPass () = if (true) then name :> UserOrPass else password :> UserOrPass
// `getUserOrPass2` binding is inferred to `unit -> (UserOrPass | Error)`
let getUserOrPass2 () = if isErr() then err :> (UserOrPass | Error) else getUserOrPass() :> _ |
Can discussion about the RFC move to the discussion thread here? fsharp/fslang-design#519 This feature is approved in principle and so it's no longer a concern of "would this ever be included, in some form, in a future F# version?". The question is how and in what shape, hence the RFC discussion. Note that there are also several open questions posed in the PR here: fsharp/fslang-design#512 |
The intention is not for multiple slots to be generated. There is still only one true implementing method; the strongly-typed overloads are simply an API facade that delegate to that.
This is definitely a valid concern. |
Where did the RFC for erased unions go? Seems like this should be the default for single-case unions. They are commonly used in the community. But they are terrible for performance as union types. |
We already have this for single cases: type Foo = int |
A type alias doesn't provide the same functionality. open System
type CourseId = Guid
type CouncilId = Guid
let courseOnly (courseId: CourseId) =
()
let courseId = Guid.Empty
let councilId = Guid.Empty
// compiles, but (helpfully) would not if CourseId were an SCU
courseOnly councilId I don't use SCUs, but many do. Understandably so. I made the above mistake. And spent a while scratching my head at unexpected results. |
Yes I had the same question. You could basically represent it as the underlying type for that case say age or name, and then break out the function by case into separate functions that are called . [<Erased>]
type Person = Age of int | Named of string
let x = Age 7
let y = Named "steve"
let process a =
match a with
| Age n -> if n > 33 then "older than voronoipotato" else "not older than voronoipotato"
| Named s -> "hello" + s
process x
process y Would get unfolded at compile time into let x = 7
let y = "steve"
let process_Age n = if n > 33 then "older than voronoipotato" else "not older than voronoipotato"
let process_Named s = "hello" + s
process_Age x
process_Named y This is what I had in mind with erased unions. Put a guid on the end if you're worried about name collision. I don't know if this is the correct way to do it... maybe you'd like use a struct and force the fields to overlap or some other witchcraft. The point is it ought to be possible... @dsyme would this be a whole new suggestion, and has it been suggested? |
to add to the conversation, allowing literal as types is allowing mixing data with types, forming a strong association with logic and domains, while this data can be anything including implementation. Instead of : type Operations =
| List
| Add
let Perform (operation:Operations) =
match operation with
| List _ -> seq { 0..1}
| Add _ -> ()//this function can return multiple types Now : //whatever the 'Literals as types' syntax are
type Operations =
| List of seq {0..1}
| Add of ()
let Perform (operation:Operations) =
match operation with
| List x -> x
| Add x -> x
//more complicated version would be taking data in this function and doing something with it people can look the code see there is operation Add & List, and if they want to add another operation called delete it's very straight forward just write it down as another case of Operations. Of course, writing implementation directly in type definition is too verbose sometimes, of course you can do it but of course Add happens to be a little bit more complicated than you think as it's adding something to somewhere on the internet, so you write the implementation somewhere separately maybe in another module |
C# feature that is similar, being discussed here: dotnet/csharplang#7544 |
Another update to the C# feature: https://github.com/dotnet/csharplang/blob/main/proposals/TypeUnions.md#ad-hoc---ad-hoc-unions |
@dsyme any chance we get this in F#? |
We probably want to wait to see what direction it goes in C# so we can ensure interoperability, etc. |
It is approved in principle, so, nothing stops anyone from implementing it. The question is that if we do it now, C# (and possibly runtime) might have different idea about their implementation, which will mean records and tuples all over again - different (potentially incompatible) implementations. |
The C# proposal as of now treats the anonymous unions as |
Modified Proposal (by @dsyme)
This is a suggestion to add adhoc structural type-tagged union types where
(A|B)
orTyped(A|B)
orTyped<A,B>
object
Typed
) or type-directed or bothSee #538 for original proposal. e.g. this would allow some or all of these:
There are plenty of questions about such a design (e.g. can you eliminate "some but not all" of the cases in a match? Is column-polymorphism supported?). However putting those aside, such a construct already has utility in the context of Fable, since it corresponds pretty closely to Typescript unions and how JS treats values. It also has lots of applications in F# programming, especially if the use of
Typed
can be inferred in many cases.Now, this construct automatically gives a structural union message type , e.g.
and a combination of update1 and update2 would give
As noted in the comments, some notion of column-generics would likely be needed, at least introduced implicitly at use-sites.
Original Proposal (@alfonsogarciacaro)
I propose we add erased union types as an F# first citizen. The erased union types already exist in Fable to emulate Typescript (non-labeled) union types:
http://fable.io/docs/interacting.html#Erase-attribute
Note that Fable allows you to define your custom erased union types, but this is because it's painful to type a generic one like
U2.Case1
. If the compiler omits the need to prefix the argument, this wouldn't be necessary and using a generic type can be the easiest solution.The F# compiler could convert the following code:
Into something like:
Pros: It will make the Fable bindings generated from Typescript declaration files much more pleasant to work with.
Cons: It's a feature that seems to be exclusively dedicated to interact with a dynamic language like JS.
Estimated cost (XS, S, M, L, XL, XXL): S
Alternatives
For Fable it's been suggested to generate overloads in the type bindings instead of using erased union types:
However these has some problems:
Affadavit
Please tick this by placing a cross in the box:
Please tick all that apply:
The text was updated successfully, but these errors were encountered: