fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

Make it optional to repeat the constraints in extensions #1390

Open Tarmil opened 5 days ago

Tarmil commented 5 days ago

I propose we make it optional to repeat the constraints on type parameters when writing an extension on a generic type.

type Foo<'T when 'T : struct> = { x: 'T }

// OK
type Foo<'T when 'T : struct> with
    member this.Y = 1

// Currently: error FS0957: One or more of the declared type parameters for this type extension
//   have a missing or wrong type constraint not matching the original type constraints on 'Foo<_>'
// Proposal: make this valid
type Foo<'T> with
    member this.Z = 1

The existing way of approaching this problem in F# is to repeat all constraints. However this is not always possible:

This is actually causing a regression in F# 9, as mentioned here. The problem is that .NET 9 introduces a new constraint allows ref struct which cannot (yet) be expressed in F#. So types that have this constraint currently cannot be extended. This is especially problematic because this constraint has been added to existing standard library types such as IEnumerable<T>.

// F# 8: OK
// F# 9: error FS0957
type System.Collections.Generic.IEnumerable<'T> with
    member _.Foo = ()

Pros and Cons

The advantages of making this adjustment to F# are

The disadvantages of making this adjustment to F# are

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Related suggestions: N/A

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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

T-Gro commented 5 days ago

The motivating example with extending "allows ref struct" is by itself a reason to resolve this - I agree.

Even though we might add syntactical support for "allows ref struc" some time in the future, having a forward-compatible handling of yet-unknown constraints is good., especially in order to avoid syncing challenges with C# or runtime.

T-Gro commented 5 days ago

It of course follows that the constraints would have to be copied over and still checked, not just ignored. Which is tricky for "allows ref struct" since F# will not be able to enforce it.

(e.g. prevent creation of a 'T array)

charlesroddie commented 3 days ago

Is this issue related?

// old code
type Dictionary<'Key, 'Value when 'Key: not null> with // FS0957 ... the type parameter 'Key requires a constraint of the form 'Key: not null...
    member t.TryGetValueSafe(k: 'Key) =
        let (b, v) = t.TryGetValue(k)
        if b then ValueSome v else ValueNone

type Dictionary<'Key, 'Value when 'Key: not null> with... // same error

Whatever is causing these things making constraints optional would be a possible temporary workaround, much better would be to express these type constraints in F#.

T-Gro commented 23 hours ago

"not null" constraint can be expressed, this is the syntax for doing a type augmentation for a Dictionary:

module TypeAugmentDictionary =
    open System.Collections.Generic
    type Dictionary<'TKey,'TValue when 'TKey:not null> with
        member x.WhatEver() = x.Count

Even when optional in a possibly new syntax, the constraints should still be typechecked - e.g. it should not be alowed to declare a local of TKey type and assign null to it, because this is not a support operation.

T-Gro commented 23 hours ago

I think it makes sense to think about constraints and anti-constraints ("allows ref struct") separately in the context of a possible design.

In the new proposal, if the constraints are syntactically optional in a type extension:

If the idea is the latter, it will enable members of an augmentation to further restrict the reach compared to the type being augmented. Either by adding a regular constraints, or by dropping an anti-constraint.

Tarmil commented 21 hours ago

It of course follows that the constraints would have to be copied over and still checked, not just ignored. Which is tricky for "allows ref struct" since F# will not be able to enforce it.

(e.g. prevent creation of a 'T array)

For an actual constraint, like eg 'T : null, then yes definitely. For an anti-constraint such as allows ref struct, I think we can do without. The question of what would happen if you pass a ref struct is kind of moot, since right now you can't have ref struct type parameters at all in F#. And if/when that is implemented, then the anti-constraint can be implicitly added to the augmentation as a non-breaking change.

For a point of comparison, see what happens with a function definition, where constraints are also optional.


let f (s: seq<'T>) = ()
/// val f: seq<'T> -> unit   (without the "allows ref struct" constraint)

let g (s: seq<'T>) = Seq.exists isNull s
/// val g: s: 'T seq -> bool when 'T: null

To me it would make sense that augmentations work similarly in these two cases.

T-Gro commented 21 hours ago

If you would do a type augmentation for Seq<>, and the generic code would not be typechecked for meeting "allows ref struct" criteria, what would happen if someone tries to invoke that extension member e.g. on Seq<Span<char>>.NewMember... ?

We cannot do without unless the member is checked separately, and then the member would not be offered for ref structs.

charlesroddie commented 18 hours ago

"not null" constraint can be expressed, this is the syntax for doing a type augmentation for a Dictionary:


module TypeAugmentDictionary =
    open System.Collections.Generic
    type Dictionary<'TKey,'TValue when 'TKey:not null> with

OK so overall not relevant to this thread. On further analysis: with netstandard2.0 you can't have the not null constraint here, with dotnet9 you must have it, and multitargeting isn't possible with a single definition. Not a big deal to worry about since netstandard2.0 is on the way to obsolescence.