Skip to content

Add Unchecked.withNull for unconstrained C# nullable-generic interop - #20232

Open
T-Gro wants to merge 6 commits into
mainfrom
t-gro-unchecked-with-null-interop
Open

Add Unchecked.withNull for unconstrained C# nullable-generic interop#20232
T-Gro wants to merge 6 commits into
mainfrom
t-gro-unchecked-with-null-interop

Conversation

@T-Gro

@T-Gro T-Gro commented Aug 7, 2026

Copy link
Copy Markdown
Member

Relates to #17734.

Adds Unchecked.withNull : 'T -> 'T | null, an escape hatch for implementing and consuming unconstrained C# nullable-generic APIs (T? M<T>(), interface { T? GetValue<T>(int) }). Unlike Operators.withNull it places no not null/not struct constraint on 'T, so such a slot can be satisfied without FS3261.

It is inline and erases to a retype ((# "" value : 'T | null #)) — nothing lands in IL. Documented as unsafe/interop-only: 'T | null is meaningless for a struct 'T. Assigning null to a resulting mutable at a struct lowers to default(T) (ILVerify-clean through multiple generic layers); the same on a concrete struct mutable stays a compile error (FS0043).

cc @Lanayx @jwosty

Adds an inline escape hatch re-typing 'T to 'T | null with no not null /
not struct constraint, so unconstrained C# nullable-generic APIs
(T? M<T>()) can be implemented and consumed from F#.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: acdc940e-b98f-4072-b143-a3e296e12fcb
@github-actions

github-actions Bot commented Aug 7, 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
`src/FSharp.Core` docs/release-notes/.FSharp.Core/11.0.100.md

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: acdc940e-b98f-4072-b143-a3e296e12fcb
@github-actions github-actions Bot added the AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed label Aug 7, 2026
T-Gro and others added 4 commits August 10, 2026 09:36
Faithfully reproduces jwosty's IEventContext.GetValue<'T> attempt: the
explicit generic member with an inferred 'T | null return now compiles
clean under warnaserror:FS3261 thanks to Unchecked.withNull, where bare
Unchecked.defaultof reports FS3261.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: acdc940e-b98f-4072-b143-a3e296e12fcb
Mirror the sibling nonNull "Unsafely retypes ... This is an unsafe
operation." wording, and spell out why withNull is unsafe (it bypasses
the not null / not struct constraints, so 'T | null can be formed for a
struct where null is not representable and degrades to defaultof) and
that it exists for interop with unconstrained C# T? generics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: acdc940e-b98f-4072-b143-a3e296e12fcb
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: acdc940e-b98f-4072-b143-a3e296e12fcb
@Lanayx

Lanayx commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@T-Gro I'm currently on vacation, can't check properly, but will this work for wrapped cases? Like for Task<'T | null>. If not, this helper might be of little use.

@T-Gro

T-Gro commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

will this work for wrapped cases? Like for Task<'T | null>.

Yes. It works for the wrapped case.

You make the nullable value with withNull in generic code. You give the value to Task.FromResult. The return type becomes Task<'T | null>. This meets the Task<T?> slot. The generic member keeps no not null or not struct constraint.

{ new IAsyncEventContext with
    member _.GetValueAsync<'T>(index) =
        Task.FromResult(Unchecked.withNull (Unchecked.defaultof<'T>)) }  // : Task<'T | null>

Note: for a struct type, you cannot read the result back at a fixed type. F# reports FS3265 there. This is correct. A struct has no null value.

I added two tests for this.

@jwosty

jwosty commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

This is great, I just wonder if there might be a better name out there? At the risk of bikeshedding: I have a feeling that future me is going to be prone to forgetting Unchecked.withNull is. Just spitballing: Unchecked.asUnconstrainedNullable, Unchecked.withNullUnconstrained, Unchecked.asNakedNull. Plus a long name would discourage people from using it willy-nilly (a good thing). Super minor though -- all good if there isn't a better name than Unchecked.withNull

Thanks for the rapid action Tomas!

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

Labels

AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

3 participants