Vandroiy
Vandroiy

Reputation: 6223

Under what conditions is unit a type?

Before this gets marked as a duplicate: I'm aware that this question is related to various questions about compilation errors when using unit as a type argument. Some examples:

These are all running into a problem similar to this one:

type Interface<'a> = 
    abstract member MyFunc : unit -> 'a

let implementingInstance =
  { new Interface<_> with
        member __.MyFunc () = () } // Compiler error!

From what I understand, the code does not compile because a unit-returning function gets compiled with void return internally, which is an extra feature of the CLI rather than a type.

However! The following seems to satisfy the compiler:

type RecordVersion<'a> =
  { MyFunc : unit -> 'a }

let recordInstance =
  { MyFunc = ignore }

This also works if I replace ignore with a lambda or a let-bound module function.

To me, this is just another formulation of the exact same thing. (Though at odds with the F# design guidelines, which suggest to prefer interfaces over function-carrying record types.)

I'm interested in designing APIs whose users specify the behavior and types used. Therefore, I would like to avoid cases where unexpected and confusing compiler errors occur. But I'm not quite sure what to make of this. It looks like F#'s "functional" functions do treat unit as a type.

What are the exact conditions for such spurious errors with unit? Can I avoid them in my API by breaking the design guidelines and using records of functions instead of interfaces? (I wouldn't mind much, but I'm not sure if it solves the problem for good.)

Upvotes: 4

Views: 231

Answers (2)

kvb
kvb

Reputation: 55184

I believe the rule is that a method that is statically known to have return type unit will be compiled to a .NET method with return type void in the .NET type system (by statically known, I mean in contrast to a generic method or a method on a generic type which uses a type parameter as the return type). At invocations, the compiler hides the distinction between methods that return void and methods that return true unit values at the CLR level.

The problem in your example occurs because properly implementing the generic interface actually requires a unit return type at the CLR level (and the CLR does care about the distinction between unit and void). In other words, the problem occurs if and only if you want to override a method which returns a type parameter of a generic class by a method which is statically known to return unit (based on substituting unit for that type parameter). By override here, I mean either implementing abstract methods on classes or interfaces or overriding non-sealed methods on classes.

As Tamil points out, one way to work around this limitation is to ensure that you use F# functions instead of methods. Another workaround is to introduce an extra concrete class into the hierarchy which has a dummy generic type parameter (say the extra class is T<'unit>), and to return Unchecked.defaultof<'unit> instead of () wherever that would cause problems. Then you can derive an additional non-generic concrete class T from T<unit> and everything will work fine.

Upvotes: 3

Tarmil
Tarmil

Reputation: 11362

The difference between your working and non-working examples is that the non-working one is a method, and in this case (and AFAIK only this case) the F# compiler generates IL code that actually takes or returns void. In the working case, it is a property of type Microsoft.FSharp.Core.FSharpFunc<unit, unit> (aka unit -> unit) which does not get "optimized" into something taking or returning void.

So yes, using records does solve the problem for good. Another possibility would be to make an interface with a property:

type Interface<'a> = 
    // Note the parentheses to make this member a property rather than a method
    abstract member MyFunc : (unit -> 'a)

let implementingInstance =
    { new Interface<_> with
        // Must use an explicit `fun () ->` instead of a method arg list
        member __.MyFunc = fun () -> () }

If your problem was with taking unit as an argument, then it could be solved by writing a method that takes (()) as argument (ie. an explicit unit value, rather than an empty list of arguments). But for the return value I don't think there is any way to make the method work :/

Upvotes: 3

Related Questions