Mark Pattison
Mark Pattison

Reputation: 3029

How to create an F# struct with a function member and a default public constructor?

I need to create a struct type for passing to an external class that is not under my control. That class requires that my struct type has a public default constructor. I need it to have a function type as a member.

The following produces error FS0001: A generic construct requires that the type 'MyStruct' have a public default constructor:

[<Struct>]
type MyStruct =
    val callBack: unit -> unit
    new(cb: unit -> unit) = { callBack = cb }

type ExternalClass<'T when 'T : (new : unit -> 'T) and 'T : struct> () =
    member val something = new 'T()

let c = new ExternalClass<MyStruct>()

This works fine if the type of the member is changed from unit -> unit to int.

I've tried using a DefaultValue attribute, which according to the docs should work fine on a function member, but this produces two errors: error FS0765: Extraneous fields have been given values and error FS0696: This is not a valid object construction expression. Explicit object constructors must either call an alternate constructor or initialize all fields of the object and specify a call to a super class constructor.

How can I create a suitable type to meet the external class's constraint?

Upvotes: 0

Views: 498

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

The problem here is that the type unit -> unit does not have a default value. Here's a shorter repro:

[<Struct>]
type MyStruct =
    val callBack: unit -> unit

let s = MyStruct()

You get an error on the last line saying: The default, zero-initializing constructor of a struct type may only be used if all the fields of the struct type admit default initialization

The type unit -> unit doesn't admit default initialization. It must have a value of function type, and there is no such thing as "default" function value.

This is how structs in .NET work: every struct always has a default constructor, and the programmer doesn't get to implement it. Instead, the runtime initializes all fields with their default values. The idea behind this is to make allocating arrays or structs very cheap: you just zero out a memory block, and voila!

So by this logic, your callBack field must be zero-outable, but it can't be, because in F# function-typed variables can't be null.

It works fine with int, because int does indeed have a default value of zero.


Now, a "good" solution would depend on what it is you're actually trying to do. But in the absence of that information, I can suggest a couple of local workarounds.

First option - make the field have type obj (so its default value would be null) and provide a safe-ish accessor that would return an option:

[<Struct>]
type MyStruct =
    val private callBack: obj
    member this.Callback with get() = this.callBack |> Option.ofObj |> Option.map (fun o -> o :?> (unit -> unit))
    new(cb: unit -> unit) = { callBack = cb }

The Callback accessor property would then return a (unit -> unit) option, which would be Some if the function was initialized or None if it wasn't:

> MyStruct().Callback;;
val it : (unit -> unit) option = None

> MyStruct(fun() -> ()).Callback;;
val it : (unit -> unit) option = Some <fun:it@10>

Second option - wrap the callback in a nullable type:

[<AllowNullLiteral>] 
type ACallback(cb : unit -> unit) = 
    member val Callback = cb with get

[<Struct>]
type MyStruct =
    val callBack: ACallback
    new(cb: unit -> unit) = { callBack = ACallback cb }

Then:

> MyStruct().callBack;;
val it : ACallback = <null>

> MyStruct(fun() -> ()).callBack;;
val it : ACallback = FSI_0006+ACallback {Callback = <fun:it@30-1>;}

This (arguably) provides a bit more type safety at the expense of an extra allocation.

Plus, there is a possibility of getting a null, but if that's a problem, you can wrap that in an option-typed accessor too:

[<Struct>]
type MyStruct =
    val private callBack: ACallback
    member this.Callback with get() = this.callBack |> Option.ofObj |> Option.map (fun c -> c.Callback)
    new(cb: unit -> unit) = { callBack = ACallback cb }

> MyStruct().Callback;;
val it : (unit -> unit) option = None

> MyStruct(fun() -> ()).Callback;;
val it : (unit -> unit) option = Some <fun:it@38-2>

Upvotes: 2

Related Questions