fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Allow aliases in interface implementation

Open smoothdeveloper opened this issue 9 years ago • 11 comments

Consider this F# code:

type IFoo =
  abstract Bar1: int -> int
  abstract Bar2: int -> int -> int

type Foo() as this =
  interface IFoo with
    member x.Bar1 a = this.Bar2 a 0
    member x.Bar2 a b = a + b

This doesn't compile because this doesn't implement IFoo implicitly, still, it is very common to have to reference this as a specific interface repeatedly.

The proposal is to add the as {alias} optional syntax to the interface implementation in a similar manner it can be added to the type defintion:

type IFoo =
  abstract Bar1: int -> int
  abstract Bar2: int -> int -> int

type Foo() as this =
  interface IFoo as ifoo with
    member x.Bar1 a = ifoo.Bar2 a 0
    member x.Bar2 a b = a + b

Pros

Making it less cumbersome to deal with interface implementation while retaining the explicit type conversions semantics.

Cons

More OO

smoothdeveloper avatar Oct 27 '16 21:10 smoothdeveloper

You do this:

type IFoo =
  abstract Bar1: int -> int
  abstract Bar2: int -> int -> int

type Foo() =
  interface IFoo with
    member x.Bar1 a = (x :> IFoo).Bar2 a 0
    member x.Bar2 a b = a + b

which is statically typed and seems safe enough,

dsyme avatar Oct 27 '16 21:10 dsyme

@dsyme I'm aware of the work around, what I'm seeing though is that you need to do this in many places (assume those interface members are called in many places in the class ) or to bind (x :> IFoo) in the class itself.

type IFoo =
  abstract Bar1: int -> int
  abstract Bar2: int -> int -> int

type Foo() as this =
  let ifoo = this :> IFoo
  interface IFoo with
    member x.Bar1 a = ifoo.Bar2 a 0
    member x.Bar2 a b = a + b

It seems that aesthetically it would be a nice and coherent fit with the type declaration so I thought of putting the suggestion out.

I agree it is a minor enhancement.

smoothdeveloper avatar Oct 27 '16 22:10 smoothdeveloper

Yeah, the idea is sound - it's quite elegant really

dsyme avatar Oct 27 '16 22:10 dsyme

@smoothdeveloper I've marked this approved-in-principle. It's a nice idea and fits well with the language design. Not high priority though...

dsyme avatar Oct 29 '16 15:10 dsyme

Looking at this, and confirming that changes to pars.fsy are rather simple (since it's my first try):

https://github.com/smoothdeveloper/visualfsharp/commit/d13c79e5f58123b2ac68885b8f8a1536ac036069#diff-f684cd54cc6b2765be570b976a252d3c

image

I'll try to investigate how that thing is handled for class declarations, not sure the same can apply to interfaces as on class declaration, the binding is implicit, for interface, I need to introduce a private field in the class.

If I'm able to advance the implementation some more, I'll draft an RFC.

smoothdeveloper avatar Sep 26 '19 17:09 smoothdeveloper

type Foo() as this =
  let f = ifoo
  interface IFoo as ifoo with
    member x.Bar1 a = ifoo.Bar2 a 0
    member x.Bar2 a b = a + b

Should using ifoo in a let binding be allowed? I assume it should as I consider the self identifier to be similar as the this in terms of scoping.

smoothdeveloper avatar Sep 27 '19 09:09 smoothdeveloper

@smoothdeveloper If this is allowed then I assume it will be subject to the same caveats as as this regarding initialization soundness. For example this throws an exception:

type Foo() as this =
    let y = this.mkY()

    member _.mkY() = "test"

let foo = Foo() // System.InvalidOperationException: The initialization of an object or value resulted in an object or value being accessed recursively before it was fully initialized.

so I would expect this to do the same:

type Foo() =
    let y = ifoo.mkY()

    interface IFoo as ifoo with
        member _.mkY() = "test"

Tarmil avatar Sep 27 '19 10:09 Tarmil

@Tarmil, thanks and great point, that is also relevant to add to the RFC, "the behaviour with the top level self identifier in context of initialization soundness is the same".

Should your first sample (calling an intrinsic member from uninitialized this) be forbidden? or at least raise a warning?

I'm not able to see one, although I've remember seeing such warnings in other contexts.

smoothdeveloper avatar Sep 27 '19 10:09 smoothdeveloper

@Tarmil, I'm just wondering, but why is that illegal? Why is this not fully initialized? Or put in other words, from an OO perspective, where is this initialized such that it raises this error? And what is the use of as this if you cannot access it anyway?

I think an argument could be made that this ought to be fully initialized before any of the private let bindings is processed. The error suggests that's not the case.

Forgive me my ignorance here, but I rarely use as this, instead I use the 'normal' self identifier for member declarations,which doesn't exhibit this issue (nor would is be accessible in let bindings).

abelbraaksma avatar Sep 27 '19 15:09 abelbraaksma

@abelbraaksma, could you check this in sharplab:

open System
type C() as this =
    member this.M() = ()

edit: weird, this code gives the warning, but as soon as I use this in a let binding, it is gone.

and then compare without the as this, I'm surprised how this gets compiled and it seems there is potential for optimization to elide the FSharpRef, or to not add it in the first place if it can be determined it is not going to be used.

I think we are discussing about this code:

https://github.com/dotnet/fsharp/blob/f49548692d89d645115c4b05b6b8d8d7fbde5b0b/src/fsharp/TypeChecker.fs#L13259-L13265

In my mind, it would have all resolved to the this pointer, I don't understand why the FSharpRef is needed but it seems to be doing with base constructor call.

smoothdeveloper avatar Sep 27 '19 15:09 smoothdeveloper

This documentation section explains how this is happening, in the last § https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/classes#self-identifiers

The self identifier that is declared with the as keyword is not initialized until after the let bindings are executed. Therefore, it cannot be used in the let bindings. You can use the self identifier in the do bindings section.

The RFC can mention:

""" Defining a self identifier on an interface implementation will result in forcing or reusing the class level self identifier, and insert the equivalent of (selfIdentifier.Value :> InterfaceType) in the typed AST. """

@cartermp, it seems the documentation is not accurate, it says the self identifier cannot be used in let bindings, while such code actually compiles fine:

type MyClass2(dataIn) as self =
    let data = dataIn
    let a = sprintf "%d %s" data (string self)
    let b = sprintf "%d %s" dataIn (string self)
    do
        self.PrintMessage()
    
    new (a: string) = MyClass2(0L)
        
    member this.PrintMessage() =
        printfn "Creating MyClass2 with Data %d" data
        printfn "self: %s" a
        printfn "self: %s" b

let c1 = MyClass2(1L)
let c2 = MyClass2("")

Should the compiler emit a warning about usage of self identifier outside of do block or has this been relaxed on purpose in a past release of the language?

quoting an answer from @dsyme in fsharp.org compiler channel

Using "self" on the whole class is expensive and should almost always be avoided.

The reason for the extra checks is that "self" can be accessed everywhere, including in the call to the base class constructor, and in strict "let" bindings. The ref cell is a bit of a hack to make this safer - it is what it is and we won't change it now, it is normal just to avoid this use of self

It seems using the reference in a strict let binding is fine, but passing it to base constructor is failing at runtime (adjusting code snippet @dsyme gave) with same error as @Tarmil sample:

type B(c: obj) =
    member x.P = 2
and C() as this =
    inherit B(this) // exception here
    let x = this.P
    member __.P = x + 1
let c = C()
printfn "%A" c.P

I'd like a compiler warning or an error for this if there is no counter exemple where passing this would not cause runtime error.

Right now, it discourages usage of let bound functions that would close over that self identifier, and kind of pushes one to write the whole implementation of a class with mutually callable members.

smoothdeveloper avatar Sep 28 '19 04:09 smoothdeveloper