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

Clarify status of SRTP constructor constraints #1068

Open
5 tasks done
dsyme opened this issue Aug 31, 2021 · 4 comments
Open
5 tasks done

Clarify status of SRTP constructor constraints #1068

dsyme opened this issue Aug 31, 2021 · 4 comments

Comments

@dsyme
Copy link
Collaborator

dsyme commented Aug 31, 2021

I propose we clarify status of "SRTP constructor constraints".

The F# language spec defines

  • SRTP member constraints and
  • .NET default constructor constraints

These are very different things though they look related in syntax. In types:

let inline SomeThing< ^T when ^T : (static member CallMe: unit -> ^T) > () = ...
let SomeThing< 'T when 'T : (new : unit -> ^T) > () = ...

The first is an F# SRTP member constraint and the second a "real" .NET constraint emitted as .NET metadata. The first requires inline, the second doesn't. The corresponding standard invocation at callsites is as follows:

let inline SomeThing () = (^T : (static member CallMe: unit -> ^T) ())
let SomeThing () = new 'T()

These correspond to AddCxMethodConstraint and AddCxTypeMustSupportDefaultCtor in the implementation.

The problem is that the status of "SRTP constructor constraints" is left unspecified. In types these are actually disallowed:

let inline f< ^a when ^a : (new : int -> ^a) > ()  = failwith ""

  let inline f< ^a when ^a : (new : int -> ^a) > ()  = failwith ""
  ----------------------^^^^^^^^^^^^^^^^^^^^^^

stdin(8,23): error FS0700: 'new' constraints must take one argument of type 'unit' and return the constructed type

That indicates the intent of F# 2.0 was to disallow these. However in implementations a corresponding check is missing and they can arise:

 let inline f () = (^a : (new : unit -> ^a) ());;

// val inline f : unit ->  ^a when  ^a : (( .ctor ) : ->  ^a)

Note the printing is different and uses .ctor - that's because this is an "SRTP constructor constraint". and not a .NET default constructor constraint. These constraints are also not checked in the same way as .NET default constructor constraints, for example there is no requirement that the return type be the same as the type being constrained, leading to nonsense code like this;

 let inline f () = (^a : (new : unit -> ^b) ()), (^b : (new : unit -> ^a) ());;

//val inline f :
//  unit ->  ^b *  ^a
//    when  ^b : (( .ctor ) : ->  ^a) and  ^a : (( .ctor ) : ->  ^b)

We need to clarify the status of these constraints in the F# language - given that they can already arise we should allow them. If they are allowed, then decide how they can be written in types and signatures. Note the syntax when ^a : (new : unit -> ^a) is not available because it is used for .NET default constructor constraints

Proposal:

  1. The syntax ^a : (new : unit -> ^a) continues to be reserved for .NET default constructor constraints

  2. We find a new syntax e.g ^a : (member ``.ctor`` : unit -> ^a) is used for SRTP constructor constraints

  3. We give a warning to require this syntax at existing implementation calls, so

     let inline f () = (^a : (new : unit -> ^a) ());;

    gives a warning with a recommendation to become

     let inline f () = (^a : (member ``.ctor`` : unit -> ^a) ());;

No syntax for these constraints is perfect given that the when ^a : (new : unit -> ^a) syntax is already taken, and can't really be changed to

Pros and Cons

The advantages of making this adjustment to F# are to make the language uniform and allow all inferred signatures to be written.

The disadvantages of making this adjustment to F# are none

Extra information

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

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this 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
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • 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.

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.

@piaste
Copy link

piaste commented May 30, 2022

``.ctor`` in user code would be very, very ugly. Also, the name of the method is an IL implementation detail, isn't it? It wouldn't make much sense for code targeting JS, for example.

I would either keep it straightforward:

let inline f () = (^a : (constructor : unit -> ^a) ());;

or maybe, since F# constructors are first-class functions, we could treat it as such?

let inline f () = (^a : (unit -> ^a) ());;

but it looks very implicit and it's probably annoying to parse.

However, what about keeping the new syntax and special-casing it so that it 'upgrades' to a .NET constraint iff the only argument is unit? In this way, if some future version of C# finally improves the new constraint by allowing different signatures, F# will be ready and won't find itself with two ways to do the same thing.

I can't think of a reason other than truly extreme microoptimization, why one would want a purely inlined constraint instead of a .NET one. However, such a specialized scenario could be served via an equally specialized attribute on the type parameter ([<InlineConstraint>] or something).

BTW, this part of the proposal is a little confusing:

Note the syntax when ^a : (new : unit -> ^a) is not available because it is used for .NET default constructor constraints

The syntax ^a : (new : unit -> ^a) continues to be reserved for .NET default constructor constraints

However, as noted above (and as I just tested in Ionide), typing let inline f () = (^a : (new : unit -> ^a) ()) currently results in a SRTP constraint being shown in the tooltip, not a .NET one. So this would in fact be a change.

@xperiandri
Copy link

xperiandri commented Nov 26, 2023

Any updates? Will we be able to write?

type InteropModuleFactory<'InteropModule when 'InteropModule : (new : IJSObjectReference -> 'InteropModule)> (jsRuntime: IJSRuntime) =

@vzarytovskii
Copy link

Any updates? We we be able to write?

type InteropModuleFactory<'InteropModule when 'InteropModule : (new : IJSObjectReference -> 'InteropModule)> (jsRuntime: IJSRuntime) =

No updates at this point

@Lanayx
Copy link

Lanayx commented Jul 23, 2024

+1 for SRTP constructor constraint to be able to support arbitrary parameters in types, not just unit

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

No branches or pull requests

5 participants