หมายเหตุ
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลอง ลงชื่อเข้าใช้หรือเปลี่ยนไดเรกทอรีได้
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลองเปลี่ยนไดเรกทอรีได้
An inline function with statically resolved type parameters can require a member with a particular name and signature. F# 11.2 lets an in-scope extension member satisfy that requirement, even when the type comes from another assembly.
Use this capability to separate a generic algorithm from adapters for particular types. You don't need to wrap those types or add a nominal interface to them.
Note
In the .NET 11.0.100 SDK, enable <LangVersion>preview</LangVersion> where the compiler resolves extensions. See F# 11.2 language settings for the planned feature band. The path and mapping examples use an F# 11.0 library and a preview consumer; see Compile libraries and consumers.
Adapt a property for a shared algorithm
The following application code measures paths represented by two existing framework types. Both paths have a length of 8:
open System.Numerics
open Structural.Adapters
open Structural.Algorithms
let complexRoute =
[ Complex(0.0, 0.0); Complex(3.0, 4.0); Complex(6.0, 4.0) ]
let vectorRoute =
[ Vector2(0.0f, 0.0f); Vector2(3.0f, 4.0f); Vector2(6.0f, 4.0f) ]
printn (pathLength complexRoute) // 8
printn (pathLength vectorRoute) // 8
The library source defines Structural.Adapters and Structural.Algorithms. The adapters expose the same coordinate property on types with different native APIs:
type System.Numerics.Complex with
member point.Coordinates = point.Real, point.Imaginary
type System.Numerics.Vector2 with
member point.Coordinates = float point.X, float point.Y
The algorithm asks for a Coordinates property returning a pair of floating-point values:
let inline coordinates (point: ^Point) : float * float =
(^Point : (member Coordinates: float * float) point)
let inline pathLength (points: ^Point list) =
points
|> List.pairwise
|> List.sumBy (fun (a, b) ->
let ax, ay = coordinates a
let bx, by = coordinates b
let dx, dy = bx - ax, by - ay
sqrt (dx * dx + dy * dy))
At each concrete call, the compiler selects a Coordinates member, called the constraint's witness. The inline algorithm accepts either an intrinsic or extension property; empty and single-point paths have length zero.
Share an operation across collection types
Extension constraints also support static members. The library's Structural.Adapters module defines adapters using the existing List.map and Option.map functions:
type List<'T> with
static member Map(source: 'T list, mapping: 'T -> 'U) : 'U list =
List.map mapping source
type Option<'T> with
static member Map(source: 'T option, mapping: 'T -> 'U) : 'U option =
Option.map mapping source
In the library's Structural.Algorithms module, request a Map member on the complete input type:
let inline map (mapping: 'T -> 'U) (source: ^Input) : ^Output =
(^Input :
(static member Map: ^Input * ('T -> 'U) -> ^Output)
(source, mapping))
The application can then share a transformation, change the element type, and compose mapping over a nested collection:
let inline cleanNames values =
map (fun (name: string) -> name.Trim().ToUpperInvariant()) values
let batch: string list = cleanNames [ " Ada "; " grace " ]
let optional: string option = cleanNames (Some " linus ")
let missing: string option = cleanNames (None: string option)
let lengths: int list = map String.length batch
let nested: int option list = map (map String.length) [ Some "abc"; None ]
printfn "%A; %A; %A" batch optional missing
printfn "%A; %A" lengths nested
// ["ADA"; "GRACE"]; Some "LINUS"; None
// [3; 5]; [Some 3; None]
The compiler checks member names and types, not interfaces or mapping laws. This isn't higher-kinded polymorphism: no parameter represents a container constructor such as F<_>.
Operator extensions
Operators can use the same mechanism. For example, define a mapping operator for lists and options:
type List<'T> with
static member (|>>) (source: 'T list, mapping: 'T -> 'U) =
List.map mapping source
type Option<'T> with
static member (|>>) (source: 'T option, mapping: 'T -> 'U) =
Option.map mapping source
let inline (|>>) (source: ^Input) (mapping: 'T -> 'U) : ^Output =
(^Input :
(static member (|>>): ^Input * ('T -> 'U) -> ^Output)
(source, mapping))
A consumer opens StructuralOperators and uses the operator:
open StructuralOperators
let incremented: int list = [1; 2; 3] |>> ((+) 10)
let length: int option = Some "hello" |>> String.length
// [11; 12; 13] and Some 5
Built-in primitive operator solutions retain priority for matching types. An extension doesn't provide arbitrary mixed-numeric widening or replace all built-in operators.
F# 11.0 mode can accept an ordinary extension-operator declaration with FS1215, but can't use it as an SRTP solution. The operator project uses preview.
Tuple operators and context
A collection of observations and a pair of independently accumulated results can participate in the same combination protocol. The following consumer defines its generic algorithm after opening the library's adapters:
open Composition.Adapters
open Composition.Algorithms
let inline combineAll (batches: ^T list) : ^T =
List.reduce (++) batches
let observations = combineAll [["temperature"]; ["pressure"; "humidity"]]
let summary = combineAll [(2, ["temperature"]); (3, ["pressure"; "humidity"])]
printfn "%A" observations
printfn "%A" summary
// ["temperature"; "pressure"; "humidity"]
// (5, ["temperature"; "pressure"; "humidity"])
These library extensions give the operator a meaning for integers, lists, and pairs:
type System.Int32 with
static member (++) (left: int, right: int) = left + right
type List<'T> with
static member (++) (left: 'T list, right: 'T list) = left @ right
type ('T1 * 'T2) with
static member inline (++) ((a, b), (c, d)) = (a ++ c, b ++ d)
combineAll requires a nonempty list because it uses List.reduce. The operation preserves list order; the compiler doesn't enforce associativity or supply an identity for an empty input.
Native headers type ('T1 * 'T2) with and type struct ('T1 * 'T2) with extend CLR reference and value tuples. Both are F# 11.2 features and support two through seven elements; larger headers produce FS3915.
Tuples can also carry context alongside a value. The following operator appends a trail while applying a contextual function:
let priced =
(["catalogue price"], fun units -> decimal units * 12m)
<*> (["validated quantity"], 3)
printfn "%A" priced
// (["catalogue price"; "validated quantity"], 36M)
type ('Context * 'Value) with
static member inline (<*>) ((before, mapping), (after, value)) =
(before ++ after, mapping value)
type struct ('Context * 'Value) with
static member inline (<*>) (struct (before, mapping), struct (after, value)) =
struct (before ++ after, mapping value)
Tuple dot-instance calls and native function-type headers such as type ('a -> 'b) with aren't supported.
The composition library and consumer both use preview. Keep combineAll in the consumer: exporting that reduction can produce FS0193 for tuple ++. The separate path and mapping examples export their algorithms.
Return-directed construction
For an operation that introduces a plain value into a container, the output type can identify which member should construct it:
type List<'T> with
static member Return(value: 'T) : 'T list = [value]
type Option<'T> with
static member Return(value: 'T) : 'T option = Some value
let inline singleton (value: 'T) : ^Output =
(^Output : (static member Return: 'T -> ^Output) value)
let oneBatch: string list = singleton "ready"
let oneOptional: string option = singleton "ready"
// ["ready"] and Some "ready"
The consumer's result annotation selects the output type's Return extension. The input alone can't choose a container.
Each container supplies its own member. For return-only overloads on a single type, use the separate AllowOverloadOnReturnType attribute.
Compile libraries and consumers
Language settings apply per project, not transitively through references. The following matrix uses the .NET 11.0.100 SDK's settings for F# 11.2 features:
| Library implementation | Consumer use | Preview requirement |
|---|---|---|
| Declares native tuple-extension headers. | Resolves their extension operators. | Both need preview; the new header syntax itself is preview. |
| Defines ordinary extensions and exports an unresolved inline member constraint. | Supplies concrete types whose extensions solve that constraint. | Consumer needs preview. The library can use F# 11.0. |
| Solves an extension constraint internally and exposes an ordinary or already-resolved inline API. | Calls that API without further extension resolution. | Library needs preview; the consumer can use F# 11.0. |
| Solves extensions and exports constraints that the consumer also resolves through extensions. | Resolves remaining constraints. | Both need preview. |
The path and mapping library uses 11.0; its consumer uses preview. From the consumer directory, run dotnet run with a supporting F# 11 SDK.
Extension scope across assemblies
Use public extensions and open their module before defining dependent inline functions. For an imported unresolved constraint, also open the adapters in the consumer; the producer's open declarations don't transfer. An already-resolved API doesn't require the consumer to reopen its adapters.
Inference and signatures
F# 11.2 resolution defers some choices that F# 11.0 mode makes earlier. Consider an inline helper defined with a DateTime receiver:
let inline shift (date: System.DateTime) offset = date + offset
In F# 11.0 mode, this form can infer DateTime -> TimeSpan -> DateTime. With F# 11.2 extension resolution enabled, it can retain a constraint for (DateTime or ^Offset) : (static member (+) : DateTime * ^Offset -> ^Result). A consumer can then select a public, in-scope extension for a different offset type.
A consumer can't recover generality lost in an F# 11.0 library. Check exported signatures; the producer might also need F# 11.2 inference enabled.
Preview limitations and troubleshooting
- For FS0001 or FS0193, check language settings, public visibility, and adapter scope.
- If a later producer file resolves the path example for
Complex, code generation can fail with FS0971,Undefined value 'get_Coordinates'. Keep that resolution with its definitions or in a separate consumer. - Competing adapters can make member selection ambiguous.