fsharp / fsharp/fslang-suggestions

Support authoring default interface methods (DIMs) in F#

Open
#1,472 6 comments 1 reaction 0 assignees View on GitHub
area: interop area: object-programming needs rfc
Dominant language
No language data
Stars
373
Forks
21
PR merge metrics
No merged PRs in 30d

Description

**I propose we** add support for **authoring default interface methods (DIMs) in F#**, so that F# libraries can declare interfaces containing concrete default implementations, rather than only consume DIMs authored in other .NET languages.

This is a focused follow-up to [Default interface methods (#679)](https://github.com/fsharp/fslang-suggestions/issues/679). That suggestion originally covered both consumption and production, but was closed after **consumption** shipped in F# 5. Its [closing comment](https://github.com/fsharp/fslang-suggestions/issues/679#issuecomment-723661513) explicitly left creation open for future design work. This suggestion tracks that remaining authoring capability, not a request to reimplement consumption.

The primary motivation is practical library evolution and two-way .NET interoperability—not introducing a general-purpose traits or type-class system.

The proposed capability should allow:

* An F# interface to declare an instance method with a concrete default body.
* Implementing classes and object expressions to omit that method and use the default, or provide their own implementation.
* F#-authored defaults to be consumed from both F# and C# using standard .NET interface dispatch and metadata.
* Library authors to add an optional operation with a meaningful default without requiring every existing implementer to supply it immediately, within the compatibility constraints of .NET DIMs.

**Proposed syntax (for RFC discussion, not currently supported interface-authoring syntax):** reuse `abstract member` to declare a contract and `default` to supply its optional implementation, following the familiar F# class-member pattern without introducing a new keyword.

```fsharp
[]
type IMessageInterceptor =
// Required operation
abstract member BeforeSend: message: string -> string

// Optional operation with a default implementation
abstract member AfterSend: message: string -> unit
default _.AfterSend(_message) = ()

type BasicInterceptor() =
interface IMessageInterceptor with
member _.BeforeSend(message) = message
// AfterSend uses the interface default.

type LoggingInterceptor() =
interface IMessageInterceptor with
member _.BeforeSend(message) = message

member _.AfterSend(message) =
printfn "Sent: %s" message

let interceptor = BasicInterceptor() :> IMessageInterceptor
interceptor.AfterSend("Hello") // Uses the default implementation.

let identityInterceptor =
{ new IMessageInterceptor with
member _.BeforeSend(message) = message }
```

The proposed rules are:

* An explicit interface declaration is required, using `[]` or `interface … end`, to avoid ambiguity with class declarations.
* `abstract member` declares the contract; a matching `default` supplies its optional implementation. A member without a default remains required.
* Classes and object expressions may omit defaulted members or supply their own implementations using the usual interface implementation syntax.
* Defaults belong to the interface rather than becoming directly accessible members of the implementing class. For example, `BasicInterceptor().AfterSend("Hello")` would not be valid; the call must use the interface view.
* Default bodies may call other interface members through `this`, allowing useful defaults derived from required operations.
* No instance fields, constructors, or instance `let` bindings are introduced.

For example, a default could be expressed in terms of a required operation:

```fsharp
[]
type IMessageSink =
abstract member Write: message: string -> unit

abstract member WriteMany: messages: seq -> unit
default this.WriteMany(messages) =
for message in messages do
this.Write(message)
```

The final syntax and semantics should be settled through an RFC. The design must preserve existing consumption behavior and specify interface inheritance, override resolution, signature-file representation, and target-runtime restrictions. For the initial scope, defer new authoring syntax for derived-interface overrides, reabstraction, explicit base-default invocation, and broader helper-member accessibility changes. Their interactions with the initial feature still need to be considered, but the basic library-versioning use case should not require every related authoring capability. Instance state and a broader traits/type-class system are outside this suggestion's scope.

**The existing way of approaching this problem in F# is** to move the interface declarations and their default implementations into C#, require every consumer to implement new abstract members, introduce additional versioned interfaces and adapters, or provide an abstract base class.

Each workaround has a cost:

* A C# companion project forces an otherwise F# library to split its public API implementation across languages merely to emit a runtime-supported construct.
* Adding required abstract members forces downstream implementation changes.
* Versioned interfaces and adapters increase API surface and maintenance overhead.
* Abstract base classes require consumers to adopt a particular inheritance model and use their single class-inheritance slot.
* Functions, object-expression factories, and delegation remain valuable F# techniques, but do not by themselves add a default body to a published interface used by independently authored F# and C# implementations.

## Pros and Cons

**The advantages of making this adjustment to F# are**:

* **A concrete solution for library authors.** In [my comment on #679](https://github.com/fsharp/fslang-suggestions/issues/679#issuecomment-601029941), I described an F# Pulsar client library used by both F# and C# developers, with user-provided routing, authentication, and interceptor classes. Evolving those extension points without DIM authoring means breaking implementers or extracting the interface logic into C#. Consumption support does not address this publishing-side problem.
Another [case](https://github.com/Lanayx/Oxpecker/commit/e896548bcbfc013d5b02defbc509cc19f0348fae#diff-032865023bbabaf5ccf56c9983d1b60b3f6ef9f84acd07224c816e56c62d363fR31) with Oxpecker library, I had to add additional method to the interface, but now all custom implementation will face build failures, while I could have provided some stub upon addition to prevent that.
* **More complete two-way interoperability.** F# should be able to publish this kind of .NET contract, not only implement or call contracts authored elsewhere.
* **Defaults without requiring a base class.** Consumers can retain their existing class hierarchy while implementing the library's interface.
* **Type-level defaults are distinct from per-instance configuration.** As [Serentty explained](https://github.com/fsharp/fslang-suggestions/issues/679#issuecomment-868126992), passing lambdas allows instance-level variation and can require storing additional data. It is not an equivalent API model for every use case. This is a semantic distinction, not a blanket performance claim.
* **Explicit support emerged in the original discussion.** Responding to the library-versioning example, [cartermp wrote](https://github.com/fsharp/fslang-suggestions/issues/679#issuecomment-601954403): “I've come around to thinking that we should also just support producing DIMs, even though it's an inheritance-oriented feature.” That comment has **8 👍 reactions and 1 🎉 reaction** as retrieved on 14 September 2026. [realvictorprm also supported creation](https://github.com/fsharp/fslang-suggestions/issues/679#issuecomment-844398408), and [abelbraaksma explicitly suggested a separate authoring proposal](https://github.com/fsharp/fslang-suggestions/issues/679#issuecomment-849774571). These are evidence of interest in authoring specifically, not proof of design consensus or approval.

**The disadvantages of making this adjustment to F# are**:

* DIMs add complexity to interface semantics, particularly around inheritance, conflicting defaults, and reabstraction. The original discussion's concerns about implementation inheritance and F#'s preference for composition remain relevant.
* Runtime support is required. This should target runtimes that support DIMs, with appropriate restrictions for unsupported targets such as .NET Framework; it is not a proposal to add that runtime support retroactively.
* Defaults do not make every interface change safe. API authors still need to consider behavioral compatibility, member conflicts, and supported runtimes.
* Syntax, emitted metadata, signature files, diagnostics, cross-language tests, and documentation require design and implementation work.

The intended balance is an opt-in capability for publishing interoperable libraries, while continuing to recommend functions and composition where those are the better design.

## Extra information

**Estimated cost (XS, S, M, L, XL, XXL): L — tentative, subject to compiler-maintainer review.**

Although the proposal reuses familiar syntax, a complete implementation requires interface/type-kind checking, member-body checking, correct DIM metadata and IL emission, signature-file representation, runtime-target diagnostics, and tooling support. Testing must cover F# and C# consumers, classes and object expressions, generics and overloads, inherited default resolution, and binary compatibility when upgrading an interface library without recompiling existing implementations.

A restricted prototype may be substantially smaller. Including derived-interface overrides, reabstraction, explicit base-default invocation, and broader member-accessibility changes could increase the scope to **XL**. This is a scope estimate, not an engineering schedule. The original closing comment described creation work as not terribly large but identified a good design as still necessary; it does not establish a current implementation estimate.

Validation should cover F#-authored interfaces consumed by F# and C#, omitted versus explicitly implemented defaults, classes and object expressions, inherited and conflicting defaults, and a library-upgrade scenario using an implementation compiled against the earlier interface. Existing DIM-consumption tests should continue to pass.

**Related suggestions:**

* [#679 — Default interface methods](https://github.com/fsharp/fslang-suggestions/issues/679): original discussion; completed for consumption, with authoring left open.
* [RFC FS-1074 — Default interface member consumption](https://github.com/fsharp/fslang-design/blob/master/FSharp-5.0/FS-1074-default-interface-member-consumption.md).
* [F# 5 consumption implementation](https://github.com/dotnet/fsharp/pull/8628).
* [C# default interface methods specification](https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md).
* [Related draft: interfaces implementing static members](https://github.com/dotnet/fsharp/pull/16199). This PR concerns implementing static interface members inside another interface, rather than general instance DIM authoring; it should not be treated as an implementation of this suggestion.

## Affidavit (please submit!)

Please tick these items by placing a cross in the box:
* [x] This is not a question (e.g. like one you might ask on [StackOverflow](http://stackoverflow.com)) and I have searched StackOverflow for discussions of this issue
* [x] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to [the compiler and tooling repository](https://github.com/dotnet/fsharp)
* [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
* [x] I have [searched both open and closed suggestions on this site](http://github.com/fsharp/fslang-suggestions/issues) and believe this is not a duplicate

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

## 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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading RFC FS-1074, the F# 5 consumption implementation in PR #8628, and the linked C# default-interface-method specification. An RFC must settle the authoring syntax and semantics, signature-file and runtime behavior, inheritance and override rules, and cross-language validation. Done means a reviewed design and implementation plan for F#-authored defaults without regressing existing consumption.

Written by the indexing model from the issue text.

Assessment

Tech stack
fsharp
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.