fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Access modifier `type private X with ...` type extensions is ignored

Open nikonthethird opened this issue 7 years ago • 5 comments

A private type extension type private X with inside a module will be in scope after the module is opened.

Repro steps

module Test

module Inner =
    type private System.String with
        member _this.ExtensionOnString () = ()

    "".ExtensionOnString () // Works here.

open Inner
"".ExtensionOnString () // Works too. [A]

Expected behavior

The line marked [A] should fail to compile, since the type extension is private to the Inner module.

Actual behavior

The code works.

Known workarounds

Placing all extensions in another private module works.

module Test

module Inner =
    module private Extensions =
        type System.String with
            member _this.ExtensionOnString () = ()

    open Extensions
    "".ExtensionOnString () // Works here.

open Inner
// "".ExtensionOnString () but no longer here.

Related information

The same on .NET and Mono.

nikonthethird avatar Apr 01 '18 10:04 nikonthethird

I think I just realized what's going on.

Access control specifiers on type extensions have no effect whatsoever: They belong on the defined member!

If you declare an extension member as private, it will only be visible inside the module.

module Test

module Inner =
    // Access modifier does nothing.
    type (* public | internal | private *) System.String with
        member _this.PublicExtension () = ()
        member private _this.PrivateExtension () = ()

    "".PublicExtension () // Works.
    "".PrivateExtension () // Works.

open Inner
"".PublicExtension () // Works.
// "".PrivateExtension () Does not work.

So this is weird. What's the deal with type private X with extensions?

Should the access modifier print a warning?

nikonthethird avatar Apr 01 '18 10:04 nikonthethird

While reading your report, I got curious what the language spec said about it. I found the syntax:

type-name :=  
    attributesᵒᵖᵗ accessᵒᵖᵗ  ident typar-defnsᵒᵖᵗ```

and

type-extension :=  
    type-name type-extension-elements  ```

where type-extension-element is merely the with keyword and the member definitions and/or interface implementations:

type-extension-elements := with type-defn-elements end ```

Note that these are the same definitions as for types, the syntax allows interface and the like, but section 8.12 of the spec says that this is not allowed (I wouldn't have mind if this was reflected by the syntax, though).

That same section, 8.12, doesn't mention a thing about the use of access modifiers on the type (as you showed, on the member, it has the expected effect).

I would propose either of those:

  1. Fix the spec retroactively, it could say, for instance, that any modifier is ignored (@dsyme, would you agree?)
  2. Fix the compiler, while this would be my preferred way of least-surprise (if the typedef is private, the extension members are all private), it would potentially break any code that uses this (even though it has zero effect whatsoever).

If (1) is chosen, I'd second your proposal that a warning would be in-place and perhaps greying it out the same as unused variables, to have a visual indication of writing something that will not have the effect you expect.

abelbraaksma avatar Apr 01 '18 17:04 abelbraaksma

The behavior is even more surprising if we look at intrinsic and optional extensions side-by-side:

module Test
open System

type MyException () = class end

// Optional extension.
type (* public | internal | private *) Exception with
    member public _this.PublicMember () = ()

    member private _this.PrivateMember () = ()


// Intrinsic extension.
type (* public | internal | private *) MyException with
    member public _this.PublicMember () = ()

    member private _this.PrivateMember () = ()
   
let ex = Exception ()
let myEx = MyException ()

ex.PublicMember ()    // Works.
ex.PrivateMember ()   // Works.

myEx.PublicMember ()  // Works.
myEx.PrivateMember () // Error (member is not accessible).

The extensions are completely identical. The first extension is optional because it extends a type not in the same module, the second one is intrinsic because MyException is declared immediately above.

However, they behave completely different (and the intrinsic one is the sane one). The private member of the optional extension is in reality publicly accessible, but only in the current module, which should be what the access modifier private on the type extension itself should be for.

The private member of the intrinsic extension gets compiled as an actual private member, so it is really private to the type, just what I would expect reading the code. Intrinsic type extensions should not have any access modifiers, anyway, since the type they extend already has one.

nikonthethird avatar Apr 01 '18 18:04 nikonthethird

I think this is because the optional extension is scoped to the module it is contained in. That would imply that a private member is in scope when used from the same module.

However, if that were the case, the intrinsic extension should behave the same, but since they are compiled on the class itself, the result is that they aren't.

I believe this is in line with what they say in the docs, albeit confusing and your could read it the other way too, quote:

Optional extensions must be in modules, and they are only in scope when the module that contains the extension is open.

Btw, your earlier example (first comment to your OP), you showed that an optional extension member that was declared as member private behaved indeed as private. Which leaves us to:

  1. An access modifier on the type declaration of a type extension is ignored (your original issue)
  2. An access modifier on an intrinsic extension member set to private is scoped to the type and cannot be used from another location, say, a module, not even the module it is declared in (this seems sane). This is in line to how it would behave if it were declared directly on the type (i.e., not as an extension, but as a normal member).
  3. An access modifier on an optional extension member has some surprising effects. Since it is scoped to the module it is declared in, if it is declared private it can be accessed outside the type def, but within the same module. It cannot be accessed from another module (a defensible, but confusing situation).

abelbraaksma avatar Apr 01 '18 20:04 abelbraaksma

The access modifier should be taken into account, thanks for the report.

dsyme avatar Apr 12 '18 14:04 dsyme