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

Implicit way to 'RequireQualifiedAccess' for all defined DUs

Open JaggerJo opened this issue 4 years ago • 16 comments

I propose we make it possible to implicitly require qualified access for all DUs declared in a project.

The existing way of approaching this problem in F# is to add an attribute ([<RequireQualifiedAccess>]) to every DU.

Often a lot if not all DUs are annotated with the [<RequireQualifiedAccess>] attribute. This adds a lot of noise that would not be required with this proposal.

(Example of how distracting RequireQualifiedAccess everywhere is)

[<RequireQualifiedAccess>]
type InstructionStepFormError =
    | TitleIsRequired
    | ShortDescriptionIsRequired
    | LongDescriptionIsRequired

[<RequireQualifiedAccess>]
type InstructionDocumentFormError =
    | Steps of Map<Guid, InstructionStepFormError list>
    | AtLeastOneStepRequired

[<RequireQualifiedAccess>]
type InstructionMetaFormError =
    | AtLeastOneOwner
    | AtLeastOneEquipment
    | TitleIsRequired
    | TitleAlreadyExists

[<RequireQualifiedAccess>]
type CreateInstructionFormError =
    | Meta of InstructionMetaFormError list
    | Document of InstructionDocumentFormError list

This could be enabled in the Project file.

It would also require an escape hatch for when qualified access should not be required. I would propose to change the RequireQualifiedAccessAttribute so it has 2 constructors.

  • [<RequireQualifiedAccess>] -> same as [<RequireQualifiedAccess true>]

  • [<RequireQualifiedAccess true>]

  • [<RequireQualifiedAccess false>]

Pros and Cons

The advantages of making this adjustment to F# are ...

  • removes noise
  • allows developers/teams to choose their defaults

The disadvantages of making this adjustment to F# are ...

  • code is not valid on its own, context is needed to know if qualified access is required.
  • makes things more complicated

Extra information

Estimated cost (XS, S, M, L, XL, XXL): ?

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • [x] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • [x] This is not a breaking change to the F# language design
  • [x] I or my company would be willing to help implement and/or test this (If provided with guidance and a lot of time)

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

JaggerJo avatar Oct 28 '21 15:10 JaggerJo

Another thing to consider is that RequireQualifiedAccess implicit across a scope would also impact records, since it applies for them as well. It would not be good to have this only activate for DUs and still require its use on records.

I also feel like this would be better as a compiler switch / project file toggle:

fsc /requirequalifiedaccess:true

and

<RequireQualifiedAccess>true</RequireQualifiedAccess>

This would be similar to <Nullable>enable</Nullable> works in C# projects.

cartermp avatar Oct 28 '21 15:10 cartermp

Another thing to consider is that RequireQualifiedAccess implicit across a scope would also impact records, since it applies for them as well. It would not be good to have this only activate for DUs and still require its use on records.

Maybe it could be configured explicitly.

<RequireQualifiedAccess>Discriminated Unions;Records;Modules</RequireQualifiedAccess>

I also feel like this would be better as a compiler switch / project file toggle:

fsc /requirequalifiedaccess:true

and

<RequireQualifiedAccess>true</RequireQualifiedAccess>

This would be similar to <Nullable>enable</Nullable> works in C# projects.

Yup, this is what I meant by 'This could be enabled in the Project file.'

JaggerJo avatar Oct 28 '21 15:10 JaggerJo

Or maybe a way to put the attribute on a parent module so it applies to selected child items?

type RequireQualifiedAccessTarget =
    | Self = 0b0001
    | ChildRecords = 0b0010
    | ChildDiscriminatedUnions = 0b0100
    | ChildModules = 0b1000
    | AllChildren = 0b1110

type RequireQualifiedAccessAttribute =
    // new ctor:
    new(target: RequireQualifiedAccessTarget) = ...


// Usage:

// Equivalent to [<RequireQualifiedAccess>]
[<RequireQualifiedAccess(RequireQualifiedAccessTarget.Self)>]
module M1 = ...

// Equivalent to [<RequireQualifiedAccess>] on all DUs inside M2
[<RequireQualifiedAccess(RequireQualifiedAccessTarget.ChildDistcriminatedUnions)>]
module M2 = ...

// Equivalent to [<RequireQualifiedAccess>] on all DUs, records and modules inside M3
[<RequireQualifiedAccess(RequireQualifiedAccessTarget.AllChildren)>]
module M3 = ...

Tarmil avatar Oct 28 '21 16:10 Tarmil

I also feel like this would be better as a compiler switch / project file toggle:

@cartermp This would be even better!

adelarsq avatar Oct 28 '21 19:10 adelarsq

The disadvantages of making this adjustment to F# are ...

  • code is not valid on its own, context is needed to know if qualified access is required.

This should be avoided. That's why I like the above proposal of @Tarmil .

Martin521 avatar Oct 29 '21 08:10 Martin521

I like @Tarmil proposal, and I'm also a bit concerned about things I kind of recall from @dsyme about "not splitting the language".

smoothdeveloper avatar Oct 29 '21 09:10 smoothdeveloper

I prefer the project file approach over module. Project file is closer to the top and a repository would typically want to have a uniform default rather than changing it per-module. Also DUs don't all live in modules as they can live directly in namespaces.

I would use this feature and set this on all projects.

charlesroddie avatar Oct 29 '21 22:10 charlesroddie

Also DUs don't all live in modules as they can live directly in namespaces.

Note this problem is similar to [<ReflectedDefinition>] and we should make the solution the same. You can put [<ReflectedDefinition>] on a module and you should thus really be able to do that for [<RequireQualifiedAccess>]

So what about allowing these two attributes on namespace fragments, e.g.

[<RequireQualifiedAccess>]
namespace MyThings

type InstructionStepFormError =
    | TitleIsRequired
    | ShortDescriptionIsRequired
    | LongDescriptionIsRequired

type InstructionDocumentFormError =
    | Steps of Map<Guid, InstructionStepFormError list>
    | AtLeastOneStepRequired

type InstructionMetaFormError =
    | AtLeastOneOwner
    | AtLeastOneEquipment
    | TitleIsRequired
    | TitleAlreadyExists

type CreateInstructionFormError =
    | Meta of InstructionMetaFormError list
    | Document of InstructionDocumentFormError list

Another option might be an attribute targeting the assembly?

[<assembly: RequireQualifiedAccess>]

However one issue with that is that we generally process attributes in compilation order (in case they reference attribute types being defined in the assembly), so in the natural implementation the "scope" of the attribute would be from the point of "declaration" onwards.

dsyme avatar Oct 30 '21 10:10 dsyme

Side notes on other "keep track of attributes while compiling code" in case it would be the primary route for this suggestion:

  • about disabling serialization members on data types: #1005

  • there is still an inconsistency on how ExtensionAttribute works in F#: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.extensionattribute?view=net-5.0

    If you are writing a compiler that supports extension methods, your compiler should emit this attribute on each extension method and on each class and assembly that contains one or more extension methods. some discussion about it: #835

In case #1005/#1090 can be considered together, as they satisfy the same demand around controlling some F# defaults in a given scope; and maybe the issue (affects VB.NET consumers and/or F# library authors) about ExtensionAttribute can be taken care in same scope?

smoothdeveloper avatar Oct 30 '21 17:10 smoothdeveloper

so in the natural implementation the "scope" of the attribute would be from the point of "declaration" onwards.

Meaning if there are files A, B, C and D where A and C define the same namespace N, and C has [<RequireQualifiedAccess>], then B can do open N but D can't? Nor projects referencing this assembly, presumably?

What if I reference two projects that define the same namespace, one with [<RequireQualifiedAccess>] and the other without?

Namespaces are quite messy :/

Tarmil avatar Nov 02 '21 09:11 Tarmil

Namespaces are quite messy :/

Yes, in the F# spec there is the concept of "namespace fragment" to cope with this, though it's not very clear to the user. The attribute would apply to the specific namespace fragment only (that is, it wouldn't appear in IL nor even in the F# metadata blob, and would simply implicitly propagate to each type and module in the namespace fragment).

dsyme avatar Nov 03 '21 13:11 dsyme

I really like the approach in the project file where we can specify what the auto-RequiredQualifiedAccess refers to.

In our code, all DUs and almost all modules have a [<Req on it, and it just makes it hard to read. For me, [<Req is a great tool to make code more readable, especially for code reviews in pull requests on GitHub, and I'm a bit sad that it adds so much noise.

l3m avatar Sep 10 '22 09:09 l3m

@JaggerJo Here's a workaround using part of your original example that mitigates the code noise issue:

[<AutoOpen>]
module Util =
    // This module would probably be in another file
    type RQA = RequireQualifiedAccessAttribute

type [<RQA>] InstructionStepFormError =
    | TitleIsRequired
    | ShortDescriptionIsRequired

type [<RQA>] InstructionDocumentFormError =
    | Steps of Map<System.Guid, InstructionStepFormError list>

theprash avatar Sep 10 '22 09:09 theprash

@theprash I am not sure I would recommend this. RQA is a custom name that might not be clear, and an additional way to to 'the same' thing as the existing attribute.

l3m avatar Sep 29 '22 13:09 l3m

What about everything that is outside a module be RequireQualifiedAccess true by default? With a config switch of course.

adelarsq avatar Sep 29 '22 20:09 adelarsq

I would be willing to prototype something for this once it is approved in principle and we clarify the approach.

edgarfgp avatar Jul 09 '24 12:07 edgarfgp