Closed A-Manning closed 1 year ago
Type abbreviations of this form can be very useful in enforcing abstractions through abstract, private, etc.
@A-Manning Could you give a couple of detailed examples please? Many thanks
One example would be to express a relation of something to another type;
type private Related'<'a,'b> = R of 'a
type Related<'a,'b> = Related'<'a,'b>
All of the functions in the module defining Related are going to have to add and remove the R constructor a lot, whereas they wouldn't if type private Related<'a,'b> = 'a
were allowed.
These types occur frequently in F* code (where the unused parameter is erased), and can't extract to F# correctly because of this restriction - so I suppose another example would be if we want to use F# to implement dependently typed languages. For example, I might want to have a type of arrays indexed by their length, as type private indexedArray<'a,n> = array<'a>
Perhaps this should just generate a warning, rather than an error?
Another use of types of this form is to express OCaml-style phantom types.
This is a pretty neat trick!
One of my coworkers has a version of F# Compiler Services that gives warnings rather than errors for this, here https://github.com/zenprotocol/FSharp.Compiler.Service/commit/1201088355c9cc58ff244fcdc8bc98477d0df230
There are technical reasons why this wasn't allowed in F# - though you could argue about their importance, and it depends what features of F# and .NET you use
For example, consider
type X<'T> = int
let f0 (x: int) = x
let f1 (x: X<'T>) = x
let f2 (xint) = (x: X<'T>)
let f3 (x: X<'T>) = typeof<'T>
Does the compiled form of f1
, f2
, f3
have a generic type parameter or not? This is significant in F# because F# is type passing and the values of type parameters can be observed (e.g. f3
). If we erase all type abbreviations prior to generalization, then type variables disappear.
There may be ways to navigate this technical difficulty but I'm not sure, and it may be very subtle, involving extensive type annotations. There are some similarities to units of measure inference, for example.
Of course, if you never rely on the type passing capabilities of F# (don't use typeof<'T>
for example), nor binary compiled form (e.g. .NET interop to F# code) then the above is less important - it really doesn't matter if there's a type parameter in the compiled form or not.
Thanks for your response!
I think it's ok if typeof<'T>
doesn't work for unused parameters, since it is clear from the type abbreviation that <'T>
is unused, and it's also possible to give a warning when this happens. It seems to me that there is not much of a reason to be using typeof<'T>
in cases where <'T>
is unused, but I can see why this behaviour would be an issue.
For F* interop with F#, this is an annoying and pervasive issue, since allowing type-level functions and higher types means that I can write
type const 'a 'b : Type = 'a
which would have to extract to
type const<'a, 'b> = 'a
This pops up frequently in extracted F* code, and has to be handled manually somehow (this is not always possible), which is not fun! I don't think that there is much that can be done to prevent types like this from being extracted, unless it's possible to pull some tricks with measure types. OCaml allows this form of type definition, which makes it much more stable as an extraction target for F*.
What would the issues with binary compiled form be?
What would the issues with binary compiled form be?
Basically the same - would the C# consumer see a generic method or not, and if so how many type parameters
The F# compiler freely eliminates type abbreviations wherever it wants - so I'm not actually sure what would happen with things like
let f3 (x: X<'T>) = typeof<'T>
I assume it would be treated as the equivalent of
let f3 (x: int) = typeof<'T>
which is a non-generic function, and give a warning that 'T
has been instantiated to obj
. But the user will certainly be surprised by this.
@dsyme What do you think is the right solution then? If this is added to F#, users might have surprising issues when making use of type passing. If this is not, then there will continue to be issues with F interop, and most likely a fork of F# will need to be maintained specifically for F. Should this be allowed with warnings rather than errors? Should the compiler have a flag to allow this? Or should this be maintained as a fork?
There may be ways to navigate this technical difficulty but I'm not sure, and it may be very subtle, involving extensive type annotations. There are some similarities to units of measure inference, for example.
I'd be happy to work on a more sophisticated workaround, but I'm not sure what would need to be done. I'd be happy to discuss this further.
@dsyme What do you think is the right solution then?
@A-Manning I'm not sure there is a solution :) That's why we made this an error.
One way to move forward would be for you to change the error to a warning in a branch of your own, and recruit/hassle/invite/bribe people to work out what happens in various corner cases like the ones above (and find even more challenging versions), and write that up here?
What about we allow this provided the unused type parameters have an attribute Erased
- inspired by Measure
- at the type alias declaration and are always erased in compiled form? And we give specific warnings if any erased types are used in typeof
or typedefof
.
What about we allow this provided the unused type parameters have an attribute Erased - inspired by Measure - at the type alias declaration and are always erased in compiled form? And we give specific warnings if any erased types are used in typeof or typedefof.
Yes. We could probably infer that without even needing any attribute, though in general having a notion of Erased
type parameters (and indeed a first-class notion of erased types - rather than just the ones you get from type providers) would be useful in general.
Has there been any movement on this? Seems like you guys sort of agreed on a possible way forward.
@ChechyLevas My bad, I seem to have let this slip - I'll make a branch that uses the Erased
attribute ASAP
I'm closing this as it's not often requested and the above discussion shows it's technically challenging
I actually think this is fairly straightforward:
type X<'T> = int
let f0 (x: int) = x // not generic
let f1 (x: X<'T>) = x // compiled form has unused generic type parameter
let f2 (x: int) = (x: X<'T>) // not generic - gives warning that 'T has been instantiated to obj
let f3 (x: X<'T>) = typeof<'T> // compiled form has generic type parameter
let f4 (x: X<string>) = x // not generic - compiled form is equivalent to f0
f0
or f4
are especially controversial.f1
or f3
to introduce a generic type parameter, regardless of the implementation of the X<_>
type, which I may not know or control.let f2 (x: int) = (x: X<'T>)
would issue a warning in the same way as let f3 (x: int) = typeof<'T>
as described above.@dsyme Do you have any objection to the above? I'd be happy to implement it.
I still think there might be value in an [<Erased>]
attribute for generic parameters, but I think that could be a separate proposal.
I propose we allow unused parameters in type abbreviations. As it stands,
type t<'a,'b> = 'a
gives an error:Type abbreviations of this form can be very useful in enforcing abstractions through
abstract
,private
, etc. Type abbreviations of this form are allowed in OCaml.The existing way of approaching this problem in F# is to either use a constructor, as in
type t<'a,'b> = C of 'a
, or sometimes use hacks with measure types. This seems unnecessary and burdensome.Pros and Cons
The advantages of making this adjustment to F# are that it would be easier to use these types for abstraction. The construct was previously allowed, so this is unlikely to require significant changes.
The disadvantages of making this adjustment to F# are that perhaps this could be seen as obfuscatory? I think such definitions are clear though. Unused parameters are allowed in function definitions, so why not type abbreviations?
Extra information
Estimated cost XS: Since F# allowed this in the past, it doesn't seem that it should be too hard to bring this back.
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply: