Skip to content
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions RFCs/FS-1331-Allow opens in type and expression scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# F# RFC FS-1331 - Allow opens in type and expression scopes

The design suggestion [Allow the use of open at type and expression scopes](https://github.com/fsharp/fslang-suggestions/issues/96) has been marked "approved in principle".

This RFC covers the detailed proposal for this suggestion.

- [x] [Suggestion](https://github.com/fsharp/fslang-suggestions/issues/96)
- [x] Approved in principle
- [x] [Implementation](https://github.com/dotnet/fsharp/pull/18814)
- [x] [Discussion](https://github.com/fsharp/fslang-design/discussions/812)

# Summary

This RFC proposes to allow the use of `open` at type and expression scopes. Type/expression-scoped `open`s will work as same as module-scoped `open`, that is, modules or types with `[<RequireQualifiedAccess>]` cannot be opened.

# Motivation

By allowing this, we can
1. Picking a certain set of operator overloads in some tight context like `Checked`.
2. Making the `open`s span less than the whole module/namespace.

# Detailed design

1. Expression-scoped `open` is an expression that opens a module in the body expression scope, and its type is the body's type.

```fsharp
((open System
Int32.MaxValue + 1 // The body expression
): int)
let test () =
open global.System
printfn "%d" (Int32.MaxValue + 1)
open type System.Int32
open Checked
printfn "%d" (MaxValue + 1)
// In computation expressions
let res = async {
open System
Console.WriteLine("Hello, World!")
let! x = Async.Sleep 1000
return x
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Lambdas ?

[ 1..10 ]
|> List.iter (fun x ->
    open System
    Console.WriteLine($"Value: {x}"))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lambda is ok

// In type and member's definitions
type C() =
do
open System
printfn "%d" Int32.MaxValue
member _.M() = open type Int32; MaxValue
```

1. Type-scoped `open` is a statement that opens a module in the type's following scope. It can be used any type definitions, type expressions and the `with` section of a record/union/exception type.


```fsharp
type C() =
Copy link
Member

@edgarfgp edgarfgp Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please add some samples about ?

type IFace =
    abstract F : int -> int
    open System
    member _.F _ = 3
        
{ new IFace with
    open System
    member _.F _ = 3
}

type C () =
    member _.F () = 3
    open System
    member _.F _ = 3
    open System
    interface IFace with
        open System
        member _.F _ = 3
       
type A =
    | A
    open System
    member _.F _ = 3
and B =
    | B
    open System
    member _.F _ = 3

type U =
    | A
    | B
    open System
    member _.F _ = 3

type R =
    open System
    { A : int }
    open System
    member _.F _ = 3

Copy link
Contributor Author

@ijklam ijklam Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opens in types can only be on the beginning of the type definition due to the complexity of making it can applied to member/val/interface ... with following the up-to-down order.
Object expressions and interface ... with cannot contain opens.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine with the feature shipping, with none of the places @edgarfgp showcased, being supported; and giving more room for consideration about the samples and in-type-definition semantics.

Ideally, it could be used in a do block at type level, let bindings, and in members themselves (under the member implemention block, not in between in stuff like properties).

do printfn "%d" Int32.MaxValue // <- Cannot find the `Int32` here
open System
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be considered an expressions-scoped open and NOT be valid in the member?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original suggestion doesn't mentioned this, but I think the contents opened in type should be valid in members.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that makes sense. I was somehow thinking of an open inside the do body. Good that you added the extra example above for it.

do printfn "%d" Int32.MaxValue
member _.M() = open type Int32; MaxValue
member _.M2() = List<int>() // <- Cannot find the `List` here
open System.Collections.Generic
member _.M3() = List<int>()
type System.Int32 with
open type System.Math
member this.Abs111 = Abs(this)
type A = A of int
with
open System
// ....
```

# Drawbacks

TODO
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any foreseen deviations from a module level open ? I assume those can arise from the implementation.
If yes, I would like to have them listed here (inconsistency being the drawback).


# Alternatives

The original suggestion mentioned 3 ways to handle expression/type-scoped `open` + `[<RequireQualifiedAccess>]` attribute:

1. Do nothing, RQA is still respected, and this specific use case is something we just accept as not accomplishable
2. Have type- and function-scoped open declarations bypass RQA
3. Introduce a "mostly RQA" attribute that allows you to open them only in the type and function scope

This RFC is based on the first option.

# Compatibility

Please address all necessary compatibility questions:

* Is this a breaking change?
> No.
* What happens when previous versions of the F# compiler encounter this design addition as source code?
> It will fail to compile, as `open` is not allowed in type and expression scopes in previous versions.
* What happens when previous versions of the F# compiler encounter this design addition in compiled binaries?
> It should work fine, as `open`s are code-level constructs.
# Pragmatics

## Tooling

Please list the reasonable expectations for tooling for this feature, including any of these:

* Debugging
* Breakpoints/stepping
Make sure that cannot set breakpoints in the `open` statement
* Error recovery (wrong, incomplete code)
* Auto-complete
Check if the completion list after the expression/type-scoped `open` is same as the module-scoped `open`.
* Code fixes
* `AddOpenCodeFixProvider`
It should works like before, that is, append the missing `open` to the nearest module/namespace-scoped `open`s.

* `ConvertCSharpUsingToFSharpOpen`
It should works like before.

* `RemoveUnusedOpens`
It should be able to recognize `open`s on expression/type level and remove unused items.