3

Consider the following code:

type Base(x : float) =
    member this.x = x
    static member (~-) (a : #Base) = Base(-a.x)
    static member Cos (a : #Base) = Base(cos a.x)

type Inherited(x : float) = 
    inherit Base(x)

let aBase = Base(5.0)
let aInherited = Inherited(5.0)

-aBase                // OK, returns Base(-5.0)
-(aInherited :> Base) // OK, returns Base(-5.0)
-aInherited           // not OK

The last line produces an error:

error FS0001: This expression was expected to have type
    Inherited    
but here has type
    Base    

Same with cos aInherited: it gives the same error, but -(aInherited :> Base) and cos (aInherited :> Base) do work.

The error message suggests that these functions want the return type of - or cos to be the same as the argument type. This seems like an overly harsh requirement.

  • For classes that inherit from a base type that defines the operators, this is not possible unless you redefine every operator.
  • If those classes reside in an external library over which you don't have control, your options are even more limited.

Is there a way around this? In the F# source code, the cos function is defined in prim-types.fs.

Jeffrey Sax
  • 10,253
  • 3
  • 29
  • 40

3 Answers3

4

I think there is no clean way to do that.

The problem is that the signature of the original global definition of these operators returns the same type as the input, so you will not be able to add a static member that does not respect this signature without redefining the global definition.

If you create a new global definition with a less restricted signature you'll have to handle all cases, otherwise the only way I can imagine that can reuse the global definition is going through intermediate types and fight the type inference:

type Base(x : float) =
    member this.x = x

type Inherited(x : float) = 
    inherit Base(x)

type UnaryNeg = UnaryNeg with
    static member inline ($) (UnaryNeg, a       ) = fun (_         ) -> -a
    static member        ($) (UnaryNeg, a: #Base) = fun (_:UnaryNeg) -> Base(-a.x)
let inline (~-) a = (UnaryNeg $ a) UnaryNeg

type Cos = Cos with
    static member inline ($) (Cos, a       ) = fun (_    ) -> cos a
    static member        ($) (Cos, a: #Base) = fun (_:Cos) -> Base(cos a.x)    
let inline cos a = (Cos $ a) Cos

This will work for all cases and for any derived type of Base:

> cos 0.5  ;;
val it : float = 0.8775825619
> cos (Base 0.5)  ;;
val it : Base = FSI_0002+Base {x = 0.8775825619;}
> cos (Inherited 0.5)  ;;
val it : Base = FSI_0002+Base {x = 0.8775825619;}
> type Inherited2(x : float) =     inherit Base(x) ;;
> cos (Inherited2 0.5)  ;;
val it : Base = FSI_0002+Base {x = 0.8775825619;}
Gus
  • 25,839
  • 2
  • 51
  • 76
2

It gets even more interesting. I thought you might be able to use the quick, hacky solution of just redefining the operators on your inherited type and using them to call the base class operators, but even after defining the operators on the inherited type, you still get the error message in the last case (which is really strange).

type Inherited(x : float) = 
    inherit Base(x)

    static member (~-) (a : Inherited) =
        -(a :> Base)

    static member Cos (a : Inherited) =
        cos (a :> Base)

If you use this definition instead of your original one, it should at least allow you to use the operators -- but instead it gives the same error message about expecting an instance of 'Base' (which is very strange).

My guess is that you've uncovered a compiler bug, or at least an edge case in the language specification. You should email this to fsbugs at microsoft.com so they can fix this in the next version.

Jack P.
  • 11,487
  • 1
  • 29
  • 34
  • I don't think it's a bug, it's the way F# deals with operators, first look at the global scope, find there a global definition with a specific signature which force both types to be the same. Therefore the error message. – Gus Feb 24 '13 at 10:15
  • @Gustavo If that were the case, and this behavior is expected, I'd expect there to be a better error message -- something about inherited operators or whatever. Since it just reuses the standard typing-related error message, it seems (to me) like this is an edge case of operator overloading resolution that's been overlooked. – Jack P. Feb 24 '13 at 15:33
2

You could put the operators in a module:

module BaseOps =
  let (~-) (a: #Base) = Base(-a.x)
  let cos (a: #Base) = Base(cos a.x)

open the module to shadow the built-in operators. Then you're no longer constrained to the expected signatures (it also sidesteps any potential bugs involved). This is the same technique used by the Core lib for checked operators.

open BaseOps

let aInherited = Inherited(5.0)
cos aInherited // OK
-aInherited    // OK
Community
  • 1
  • 1
Daniel
  • 47,404
  • 11
  • 101
  • 179
  • Good idea for a work-around, but then something like `cos 5.0` fails with _The type 'float' is not compatible with the type 'Base.'_ Not exactly what I want, either. – Jeffrey Sax Feb 23 '13 at 15:13
  • @JeffreySax `cos 5.0` wouldn't work because, unlike C#, there are no implicit conversion operators. The F# compiler simply can't guess that `5.0` has to be first converted to `Base` and then `-` applied. – Be Brave Be Like Ukraine Feb 24 '13 at 02:59
  • @bytebuster I know that is why it fails. The problem is that this code hides the original definition of `cos`, and so causes an expression like `cos 5.0`, which would otherwise work fine, to not compile any more. – Jeffrey Sax Feb 24 '13 at 03:06
  • @JeffreySax: You need to use `cos` on `#Base` and `float` alternately or on the same line? You _can_ `open` a module multiple times to switch between operators. Might be too tedious for your situation...dunno. – Daniel Feb 24 '13 at 03:47
  • @Daniel I didn't realize you can switch module contexts that way. Still, operations on `#Base` and `float` may be commonly written in the same expression. – Jeffrey Sax Feb 24 '13 at 14:59