inheritancecompiler-errorsf#overridingfsunit

Object expressions and error FS0419: 'base' values may only be used to make direct calls to the base implementations of overridden members


The error in the title is vague but googling it gives presently two hits on SO and five altogether (suggesting it is a rare beast, so don't expect too many visits here ;). I expect this question to be sixth in that list :p.

Kvb's answer in this thread suggests that the error text is misleading and is raised if you call base from within a closure, but that doesn't always seem the case, as this works:

// in a closure, but no FS0419
let myObj = fun foo ->
    { new obj() with override this.ToString() = base.ToString() + foo}

But this fails (the simplest way I found to exemplify the issue):

// not in a closure, but raises FS0419
let myObj = 
    { new obj() with override this.ToString() = () |> base.ToString }

I was using object expressions instead of type declarations with inheritance, and I tried to create a new FsUnit constraint by building a custom NUnit constraint. Here's a simplified version that shows the issue I was seeing:

let EndsWithIgnoreWhitespaceContraint expectedResult = 
    { new EndsWithConstraint(expectedResult) with 
        override __.ApplyTo<'T> (actual: 'T) =
            let actual = box actual
            if actual :? string then 
                actual :?> string
                |> fun s -> if isNull s then String.Empty else s
                |> fun s -> s.Trim()

                // error FS0419: 'base' values may only be used to make direct
                // calls to the base implementations of overridden members
                |> base.ApplyTo 
            else
                exn "String expected .. bla bla..." |> raise        }

// make it available to FsUnit's syntax style (overriding existing endWith)
let endWith = EndsWithIgnoreWhitespaceContraint

// using it:
"  hello world  " |> should endWith "world"

Now, it is obviously not necessary to know FsUnit to see this behavior. But it took me a night and a day to realize I was duped, in fact I didn't see it until I was writing this question on SO.

Turns out that this works:

Instead of x |> base.SomeMethod write base.SomeMethod x.

I find this surprising. Not sure it is a bug or a feature. But since the |> operator is inlined (I tested it with a different operator) and it doesn't create a new function (like >> does), I don't see why this error is raised.

In fact, I don't see any semantic difference between f a and a |> f (apart from precedence rules and the like). So why the error? What rule am I breaking?


One final thought, kvb wrote "basecannot be called from a closure... curried members create a closure automatically", this suggests this would be wrong, but it compiles just fine:

let myObj foo bar = 
    { new obj() with override this.ToString() = base.ToString() + foo + bar}

Does anybody know of precisely what causes, or not causes, this error?


Solution

  • First, you misunderstood the answer that "base cannot be used in a closure". It was meant to refer to a closure that captures the base itself - it is the capturing that prevents this from working, not the closure as such. In your { new obj } example, base is not captured by any closures. The whole object is captured, but base is only directly used within the ToString method.

    To illustrate, try this:

    let myObj = 
        { new obj() with override this.ToString() = (fun() -> base.ToString())()}
    

    This code won't compile, because base is being captured by the closure fun() -> base.ToString().

    Secondly, using an object method as a function does not work "directly" as one might expect, because .NET methods are represented differently from F# functions. Instead, when faced with something like let x = obj.M, the compiler will treat it as let x = fun a -> obj.M(a) - that is, wrap it in a closure.

    To illustrate, try this:

    let myObj = 
        { new obj() with 
          override this.ToString() = 
            let f = base.ToString  // Error here
            f()
        }
    

    See where this is going? :-)

    When you pipe into an object method, the compiler has to create that closure, and then pass it to the pipe operator. To illustrate, try this:

    let myObj = 
        { new obj() with 
          override this.ToString() = 
            () |> base.ToString  // Same error
        }