Skip to content
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

Make it optional to repeat the constraints in extensions #1390

Open
6 tasks done
Tarmil opened this issue Nov 14, 2024 · 9 comments
Open
6 tasks done

Make it optional to repeat the constraints in extensions #1390

Tarmil opened this issue Nov 14, 2024 · 9 comments

Comments

@Tarmil
Copy link

Tarmil commented Nov 14, 2024

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

  • more conciseness when extending a type that has constraints.
  • working around the allows ref struct regression mentioned above, and avoiding similar future regressions.

The disadvantages of making this adjustment to F# are

  • constraints are less explicit I guess?

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:

  • This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
  • This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

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.

@T-Gro
Copy link

T-Gro commented Nov 14, 2024

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
Copy link

T-Gro commented Nov 14, 2024

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
Copy link

charlesroddie commented Nov 16, 2024

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
Copy link

T-Gro commented Nov 18, 2024

"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
Copy link

T-Gro commented Nov 18, 2024

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:

  • Are they automatically considered as if they were repeated and typechecked? This might be tricky with "allows ref struct". Not only because F# compiler does not know how to typecheck it in generic context as of now, but also because it might impose a complicated feature onto users who might have been living just fine without knowing what a "ref struct" is. (imagine wanting to write a .ToArray extension)
  • Are they not considered, and typechecking must treat the invocation of the augmentation member separately?

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
Copy link
Author

Tarmil commented Nov 18, 2024

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
Copy link

T-Gro commented Nov 18, 2024

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
Copy link

"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.

@T-Gro
Copy link

T-Gro commented Nov 19, 2024

#if for the constraint definition if augmenting a type that has different definitions across the TFM targets, I am afraid I do not have a better proposal here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants