-
Notifications
You must be signed in to change notification settings - Fork 22
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
Generate F# 9-style Is* property for single case discriminated union #1394
Comments
Not sure how I feel about this. One odd thing is that you can't polyfill it yourself if you want: type U =
| A
member this.IsA = match this with A -> true > type U =
- | A
- member this.IsA = match this with A -> true;;
member this.IsA = match this with A -> true;;
----------------^^^
stdin(3,17): error FS0023: The member 'IsA' cannot be defined because the name 'IsA' clashes with the default augmentation of the union case 'A' in this type or module I wonder if that is by design. |
It looks like the original implementation emitted this, and it was later changed not to: fsharp/fslang-design#517 (comment) The RFC itself does not mention it: https://github.com/fsharp/fslang-design/blob/3a46bc9342f4c2c38d09617b4d95909697e7b9d6/RFCs/FS-1079-union-properties-visible.md |
Interesting. Doesn't seem like there was a ton of discussion involved - just "this feels weird" and then it gets removed. I think we should revisit that. Having it missing for single case unions feels like a "gotcha". But that is also good news in that the exact place to implement this is straightforward. |
To be fair, @T-Gro did mention the main reason why:
and, in dotnet/fsharp#16571,
I.e., the F# compiler already did generate But the compiler did not generate an |
Yes, that makes sense. Then this could probably be added as a con to implementing this.
I do think the consistency it would bring would be worth the effort. |
Not generating the warning in the described case is doable, that was definitely an oversight and mismatch in codegen vs. warning. I must admit I do not fully see the motivation for practical F# programs. let workWithImage Image = 42
let workWithImages imgList =
match imgList with
| Image :: _rest = 42
| [] -> 2112 |
Regularity is good so this should be added. // This code is OK and remains OK when more shape cases are added.
type Shape = | Rectangle of Dimensions
let countRectangles(shapes: Shape list) =
shapes |> List.countWhere(s -> s.IsRectangle)
But you don't need big use cases to justify making the langauge more regular. Even if there were no use case it should be there! |
If you would have hand-written logic for .IsRectangle, you could amend it if/once future extension arrives. I am the kind of person who would rather get a warning and adjust logic to new requirements case by case. Rather then having no new warnings, but wrong business logic caused by pre-existing usage of .IsRectangle . (because once new requirements were added, counting rectangles might had to include squares as well). But you are right that this equally applies to any number of cases, not only size=1. Usage for size=1 just makes it apparent that the intention is to prepare for unknown future/possible requirements, which I guess is the part which feels wrong about this. |
Yes. YAGNI. |
Yes it does make that apparent, as it should as this is a definition of a DU aka discriminated union. So it feels wrong that the discrimination scaffolding isn't there, including
And yes to YAGNI - sometimes discriminated unions shouldn't be used but instead the underlying data type or a simpler non-discriminated non-union wrapper used, for situations in which it's not expected that cases will be added. But this doesn't mean that it's never useful to start with the possibility of discrimination and so there shouldn't be a restriction on DUs that they should have at least 2 cases. E.g. I might know I am going to write more Shapes, and I might want my serialization/deserialization of Shapes to conform to a DU structure so that it doesn't change when adding more cases. Given that we are not going to ban DUs with one case, ensuring that they are treated as DUs and not something special is important for consistency. |
Disagree here, but I think you put it well:
Some additional context might be helpful - I am a relative newcomer to F#, I've only just recently started writing meaningful amounts of F# code. When I saw the Is* feature as added in F# 9, I immediately had an intuition as to how it should behave based on how the feature was advertised. Namely:
let canSendEmailTo person =
match person.contact with
| Email _ -> true
| _ -> false
let canSendEmailTo person =
person.contact.IsEmail Nothing in this presentation makes me think "instead you can write this, but only in the case of a DU with at least 2 cases" It's a small inconsistency, sure. It's also one that, if fixed, arguably might not necessarily benefit a vast number of use cases. But it's an inconsistency nonetheless. And in my opinion, small inconsistencies are the ones that are the most off-putting for me as a user of a language. |
I think we should allow to polyfill that function yourself, and rework the existing warning to only warn for #cases>1. I am still not convinced that the compiler should automatically generate it for every single DU (of size 1). Single case DUs are a very common data modelling tool and this just increases the surface area (both F#-visible as well as in the generated IL) for a use case that does not have strong support, and encourages a programming style that is not always recommended. |
Agreed. At a minimum we should do this.
F# supports object programming, but I would hardly say it encourages that style. It could also be argued that having
Indeed. I would say that DUs in general are. Why treat the single-case as something special? That's ultimately what this boils down to for me: they're treated as special -- which constitutes an inconsistency in the language -- but are not particularly well-documented as such. |
Agreed.
Single-case unions are special, though. There are no other cases that they could be, and this fact is known at compile time. It is always synchronically redundant to ask which case an instance of a single-case union is. I don't find the "what if more cases are added to the union in the future" argument compelling:
The upshot to me feels like:
That seems like a pretty limited amount of utility, and I think there are nearly always better ways to do things anyway.
I disagree with this. I don't think we should increase code size without the utility to justify it. |
We agree that the You say that
You mention this consideration was the reason originally not to include these methods. This is not a cost in any context where this is important. Dotnet trimmers are very good for any type-safe contexts, including F#. The only case where you would deploy something without trimming is if you don't care about compiled code size. If you don't use
An example of my point about consistency even without use cases is the code |
We have to maintain this as absolute: A discriminated union is discriminated To argue that a we should have single-case discriminated unions and at the same time argue that they have no need to discriminate and that all discrimination logic can be removed is nonsensical. Can you use a discriminated union if you just have one case and have no intention to discriminate any point in the future? Yes it's possible. You just don't use the discrimination features in any useful way. I would strongly recommend not doing so since it is better to use a non-discriminated type. A single-field record is better and so is a struct containing a single value. Both are similar to using a single-case DU without the discrimination (matching, Is) and discrimination is not required. And even if this were to become common, it should in no way stop discriminated unions from being discriminated! "I used a type called discriminated something; please can you remove discrimination costs from this because it's not useful to me" is an argument that should therefore have zero weight. A similar argument has been made for measures, "I want to use F# units of measure for type safety and instead of a record or class; please can you remove the constraint that measures apply to types that can be multiplied?" Similarly breaking the intent and meaning of the original feature. |
The recommended way remains pattern matching. It works for unions of any size, and if number of cases grows from 1 to 2 or more, all places pattern matching it will show a warning. The rare and opinionated cases that require '.IsX' (always returning true) in their single-case-DU scenarios should be allowed to opt-in into that, I agree. The warning should be lifted and definition of such custom member allowed. |
I agree that in general generating |
I propose we generate an Is* boolean property for single case unions, as F# 9 does today for multi-case unions.
The existing way of approaching this problem in F# is to add a throwaway associated value and pattern match, like so:
Pros and Cons
The advantages of making this adjustment to F# are that the property is generated for all cases of a DU, not just when there are multiple. While in theory the Is property is redundant for that case since it is guaranteed to be true, this enhancement improves the behavior for "planning for future cases" by allowing to use the same syntax, instead of having to do a hacky workaround just for the single case scenario
The disadvantages of making this adjustment to F# are that perhaps this is a small thing and maybe the juice isn't worth the squeeze just for this extra consistency?
Extra information
Estimated cost (XS, S, M, L, XL, XXL): S to M
Affidavit (please submit!)
Please tick these items by placing a cross in the box:
Please tick all that apply:
For Readers
If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
The text was updated successfully, but these errors were encountered: