Fsharp: Problematic differences in constraint handling for structs between C# and F#

Created on 7 Dec 2019  路  6Comments  路  Source: dotnet/fsharp

Nullable constraints are 'T (requires default constructor and struct and :> ValueType)

// Compiles
public void M(Nullable<(int, string)> foo) {
}

// Compiles
public void M<Foo>(Nullable<(int, Foo)> foo) {
}
// Errs: The type 'struct (int * string)' is not compatible with the type 'ValueType'
let m (foo: Nullable<struct(int * string)>) = Nullable.op_Implicit(null)
// No difference
let m (foo: Nullable<ValueTuple<int, string>>) = Nullable.op_Implicit(null)

// Errs: A generic construct requires that the type 'struct (int * 'Foo)' have a public default constructor
type T<'Foo>() = 
    let m (foo: Nullable<struct(int * 'Foo)>) = Nullable.op_Implicit(null)

Area-Compiler Severity-Medium bug

All 6 comments

I'm also not sure where Nullable's requires default constructor constraint comes from, in C# it's just constrained as follows <T> where T : struct

This means any type argument to nullable where said type doesn't have a default constructor would actually cause that last error.

Confirmed, no way to create * generic * structs in F# that will fit Nullable's constraints (and I assume any other type with the same constraints) if used in a generic function/method.

F# defined structs only seem to work for Nullable if all arguments are concrete.
It seems the compiler wants to inductively check the type before deciding if the type itself satisfies the default constructor constraint? Obviously it would fail if some parameters are still free. But that behavior doesn't make a lot of sense to me.

type [<Struct>] Foo<'bar>(bar: 'bar) = struct end
// Compiles
let m (foo: Nullable<Foo<int>>) = Nullable.op_Implicit(null)
// A generic construct requires that the type 'Foo<'bar>' have a public default constructor
let m (foo: Nullable<Foo<'bar>>) = Nullable.op_Implicit(null)

// C# type
// Compiles
let m (foo: Nullable<Memory<int>>) = Nullable.op_Implicit(null)
// Compiles
let m (foo: Nullable<Memory<'bar>>) = Nullable.op_Implicit(null)

I'll change the title...

This seems mostly like a bug to me, but I'd like @dsyme to chime in about this behavior to see if it's intended, since the introduction of Nullable Value Types (I'm calling them this now, instead of the legacy "Nullable Types" name) came quite a while back and there doesn't appear to be anything about this in the spec.

Yes, those should both be fixed AFAICS

This seems to be similar to https://github.com/dotnet/fsharp/issues/7618, generally speaking.

There's work to improve this here https://github.com/dotnet/fsharp/pull/10006. I've linked this bug there to make sure we capture all the test cases

Confirmed, no way to create * generic * structs in F# that will fit Nullable's constraints (and I assume any other type with the same constraints) if used in a generic function/method.

@NinoFloris #10006 completes this.

That said, when instantiating Nullable with generic structs and generic struct anonymous records beware that

  1. Nullable forces a default value (e.g. a default constructor) on to it's type argument., so the struct tuple type must have a default value

  2. This means each element type of the struct tuple/anon-record must have a default value.

Now, F# is fairly strict about when types have default values.

  • Prior to the fix in #10006 generic type variables were never considered to have default values for the purposes of this analysis.

  • Even after #10006 a type variable must still have an explicit annotation (or inferred constraint) that the type is either : struct or : not struct - it can't be neutral about this.

  • If : struct then a default constructor constraint : (new : unit -> 'T) is also required

  • If : not struct then a : null constraint is required.

Was this page helpful?
0 / 5 - 0 ratings