Skip to content

Refactoring between F# and .NET-compatible optional parameters - #20547

Open
xperiandri wants to merge 4 commits into
dotnet:mainfrom
xperiandri:feature/optional-parameter-default-value-refactoring
Open

xperiandri wants to merge 4 commits into
dotnet:mainfrom
xperiandri:feature/optional-parameter-default-value-refactoring

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Description

Adds an editor refactoring (Ctrl+. on a member's optional parameter) that switches it between the F# form and the .NET-compatible form:

type Greeter() =
    member _.Greet(name: string, ?greeting: string) =
        let greeting = defaultArg greeting "Hello"
        $"{greeting}, {name}"

open System.Runtime.InteropServices

type Greeter() =
    member _.Greet(name: string, [<Optional; DefaultParameterValue("Hello")>] greeting: string) =
        $"{greeting}, {name}"

?x: T is optional only for F# callers; [<Optional; DefaultParameterValue(c)>] x: T is optional for C# and VB callers as well. F# call sites need no change for either form (M(), M(x = 1) and M(?x = opt) all compile against both), so only the member itself is edited.

To [<Optional; DefaultParameterValue(c)>]. Every use of the parameter in the member body must be defaultArg x c with the same constant c, of a type that matches the parameter's annotation (so the attribute does not trigger FS3211). A line that only rebinds the parameter, let x = defaultArg x c, is deleted; any other defaultArg x c becomes x. A parameter with no uses becomes [<Optional>] x: T. open System.Runtime.InteropServices is added when the file does not have it.

Back to an F# optional parameter. Two actions are offered:

  • "Use F# '?' optional parameter": the Optional/DefaultParameterValue attribute lists are removed, ? goes back in front of the name, and let x = defaultArg x c becomes the first line of the body;
  • "Use F# '[] ?' optional parameter", from F# 10 (LanguageFeature.SupportValueOptionsAsOptionalParameters): the same, with [<Struct>] ?x: T and let x = defaultValueArg x c.

A body written on the member's own line moves below it. Without DefaultParameterValue the default is Unchecked.defaultof<_>.

Not offered for an unannotated ?x, a [<Struct>] ?x (towards the .NET form), uses of the parameter other than defaultArg x c, differing or non-constant defaults, attribute lists that hold other attributes too, overrides, interface implementations, abstract/default slots, constructors, extension members, or a file with a paired .fsi.

Covered cases

Converts

  • Shadowing default, adding the missing open: ?greeting: string with let greeting = defaultArg greeting "Hello"[<Optional; DefaultParameterValue("Hello")>] greeting: string, the let line removed, open System.Runtime.InteropServices inserted; calls Greet("Ada") and greeting = "Hi" stay as they are
  • Shadowing default round trip, both ways, when the open already exists: ?step: int + let step = defaultArg step 1[<Optional; DefaultParameterValue(1)>] step: int; the same for a second parameter ?step: float with 0.5
  • Inline default: value + defaultArg step 1[<Optional; DefaultParameterValue(1)>] step: int) = value + step
  • Back with a body on the member line: step: int) = value + step?step: int) =, then let step = defaultArg step 1 and value + step on their own lines
  • Parameter with no default use: ?flag: bool (unused) → [<Optional>] flag: bool
  • Back with no DefaultParameterValue: [<Optional>] step: int?step: int + let step = defaultArg step Unchecked.defaultof<_>
  • Back to a struct optional parameter: [<Optional; DefaultParameterValue(1)>] step: int[<Struct>] ?step: int + let step = defaultValueArg step 1; [<Optional>] step: int… defaultValueArg step Unchecked.defaultof<_>
  • Action titles: the F# form offers only Use [<Optional; DefaultParameterValue>] for optional parameter; the .NET form offers Use F# '?' optional parameter, then Use F# '[<Struct>] ?' optional parameter
  • Language version gate: before F# 10 (--langversion:9.0), the .NET form offers only Use F# '?' optional parameter
  • Each converted document type-checks without errors

Not offered

  • Different defaults: defaultArg x 0 + defaultArg x 1
  • A use other than defaultArg: static member M(?x: int) = x.IsSome
  • Default that is not a constant: ?x: obj with defaultArg x (box 1)
  • Constant whose type does not match the parameter: ?x: int with defaultArg x 1.0
  • Parameter with no type annotation: static member M(?x) = defaultArg x 0
  • Struct optional parameter: [<Struct>] ?x: int with defaultValueArg x 0
  • Extra attribute in the list: [<Optional; In>] x: int
  • Override of an abstract slot: abstract M: ?x: int -> int with default _.M(?x: int) = defaultArg x 0
  • Ordinary parameter: static member M(x: int) = x
  • File that has a signature file: static member M: ?x: int -> int

Part of a series of optional-parameter and struct refactorings: #20546 switches ?x and [<Struct>] ?x, #20545 does option ⟷ struct for partial active patterns.

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes (not applicable)
  • Release notes entry updated

🤖 Generated with Claude Code

…meters

?x: T is optional for F# callers and gives the body an option;
[<Optional; DefaultParameterValue(c)>] x: T is optional for C# and VB callers
too and gives the body the value. The refactoring moves the default between
defaultArg x c in the body and the attribute, adds the InteropServices open
when it is missing, and leaves call sites alone: F# accepts both forms the
same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri and others added 2 commits September 14, 2026 20:03
…form

[<Optional; DefaultParameterValue(c)>] x: T now converts back to either
?x: T with let x = defaultArg x c, or, from F# 10, [<Struct>] ?x: T with
let x = defaultValueArg x c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e of its section

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xperiandri
xperiandri marked this pull request as ready for review September 14, 2026 19:40
@xperiandri
xperiandri requested a review from a team as a code owner September 14, 2026 19:40
@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Modifies Visual Studio code executed while projects are open.

Generated by PR Tooling Safety Check · gpt56 1.8M ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant