Open agocke opened 1 year ago
Not really. The anti-constraint is about cancelling out a constraint that already exists. The way it currently works is that .NET has an implicit heap
constraint that's available to all unconstrainted type parameters. That constraint allows you to do things like box variables of that type, and store variables of that type as fields in classes and structs.
The "allows ref struct" anti-constraint cancels out the implicit heap
constraint and removes it from the constraint list. It's not about adding constraints to an implicit union, it's about removing them.
The \"allows ref struct\" anti-constraint cancels out the implicit heap constraint and removes it from the constraint list. It's not about adding constraints to an implicit union, it's about removing them.
It is indeed an intersection from that perspective, just like adding class?
→ mightbenull
, I suppose, except mightnotbeboxable
.
While it is removing a constraint from the consumer's perspective, it is also adding a constraint by which the declaring construct must abide...
...Upon further thought, though, I think I'm back to where I was before. (Sorry for messing up the threading; I was on my phone.)
I think we were using the same metaphor to refer to different things, or rather referring to the same thing from different perspectives (API consumer versus API implementer).
Given my previous post:
In a way, adding an "anti-constraint" is really just adding a case to the implicit default constraint union
T
≈where T : default
≈where T : (class | struct)
:If —
void M<T>(T t) { … }
≈
void M<T>(T t) where T : default { … }
≈
void M<T>(T t) where T : (class | struct) { … }
— then
void M<T>(T t) where T : allows ref struct { … }
≈
void M<T>(T t) where T : (class | struct | ref struct) { … }
≈
void M<T>(T t) where T : (default | ref struct) { … }
And your response:
The "allows ref struct" anti-constraint cancels out the implicit
heap
constraint and removes it from the constraint list. It's not about adding constraints to an implicit union, it's about removing them.
This emerges from the fact that the set of operations that can be applied to a union is the intersection of the operations that can be applied to every case in the union.
From my other post (again, sorry for messing up the thread):
Yeah, that's what my imaginary "constraint union" syntax was supposed to mean —
class
constraint (and all that implies, including ability to be boxed on the heap) orstruct
constraint (and all that implies, including ability to be boxed on the heap) orref struct
constraint (and all that implies, including inability to be boxed on the heap). Any concrete type parameter can and must satisfy exactly one of those at a time, i.e., it is either a class, a regular struct, or a ref struct.
But by definition in a generic construct the type parameter is not yet concrete, so, since you can't box a ref struct
, you can't box a T
where T : (class | struct | ref struct)
, since T
might end up being instantiated to a ref struct
.
Seen that way, it actually makes sense that F# might not auto-propagate this constraint unless explicitly specified, since, unlike all existing constraints utterable in C# or F#, it is not being intersected with the implicit (class | struct)
union of constraints.
Given two functions like this (imaginary syntax; F# does happen to already use and
to represent constraint intersection, so it would probably make sense to use or
to represent constraint union):
let f (x : 'T when 'T : struct or 'T : not struct or 'T : byref<struct>) = ignore x
let g x = f x
It would make sense that g
would have the more general signature:
val f : x:'T -> unit
i.e., with the default, implicit constraint union
val f : x:'T -> unit when 'T : struct or 'T : not struct
since 'T : struct or 'T : not struct
[^1] is of course a subset of 'T : struct or 'T : not struct or 'T : byref<struct>
.
Just because f
has placed the restriction upon itself that T
might be something unboxable doesn't mean that g
must also have that restriction.
[^1]: not struct
is F#'s equivalent of the C# class
constraint.
ref record struct
was previously not possible since ref struct
couldn't implement IEquatable<S>
#5431
Would just the implementation of this proposal allow the compiler to synthesize the record
members or would that be a separate proposal? @AlekseyTs
Would just the implementation of this proposal allow the compiler to synthesize the record members or would that be a separate proposal?
That would need to be a separate proposal. The inability to implement IEquatable<T>
is just one of the issues that would need to be addressed before we could do ref record struct
. Guessing when we sit down and look at all of the behaviors around record
we'd find a few more we'd need to think about.
One of the marquee original intentions for
ref struct
s implementing interfaces was forSpan<T>
andReadOnlySpan<T>
to implementIEnumerable<T>
. This would help solve a number of betterness issues with adding new APIs, but becauseIEnumerable<T>.GetEnumerator()
returns anIEnumerator<T>
, we can't implement it in an allocation-free manner. That would cause anyIEnumerable
API to become a performance trap forSpan<T>
, which is extremely undesirable.
Just add the IEnumerator<T>
interface on the Span<T>.Enumerator
type. I don't need the IEnumerable<T>
interface on the span itself for now. Anyway, that could be saved for a future improved IEnumerable<T, TEnumerator>
interface after C# improves generic inference for those.
My use case that I was hoping to start using this for:
public static Promise All(params ReadOnlySpan<Promise> promises)
=> All(promises.GetEnumerator());
public static Promise All<TEnumerator>(TEnumerator promises) where TEnumerator : IEnumerator<Promise>, allows ref struct
{
using (promises)
{
...
while (promises.MoveNext())
{
var p = promises.Current;
...
}
return ...;
}
}
Rules for
ref struct
s in generics are approved. Rules forref struct
s implementing interfaces generally look good, but need validation against real world scenarios before we allow this part of the feature to ship in anything more than preview.
Allowing ref struct as generics but disallowing them to implement interfaces kind of neuters the feature.
Allowing ref struct as generics but disallowing them to implement interfaces kind of neuters the feature.
While I agree that having this held back behind the preview language feature is undesirable, it's still available to the people who really want to use it. For example, I rewrote my HTML generation library to make use of the features.
Ref structs implementing interfaces
Related Issues
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-26.md#ref-structs-in-generics https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-10.md#ref-structs-implementing-interfaces-and-in-generics https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-07-22.md#ref-structs-implementing-interfaces