SRTP 約束的擴展成員

具有 靜態解析型別參數 的內聯函式,可能需要具有特定名稱與簽名的成員。 F# 11.2 允許範圍內的擴展成員滿足該要求,即使該型別來自其他組件。

利用此功能將通用演算法與特定類型的轉接器分離出來。 你不需要包裝這些類型,也不需要加裝名義上的介面。

Note

在 .NET 11.0.100 SDK 中,啟用<LangVersion>preview</LangVersion>編譯器解析擴充功能的地方。 請參閱 F# 11.2 語言設定 以了解計畫中的功能頻段。 路徑與映射範例使用 F# 11.0 函式庫與預覽消費者;參見 編譯函式庫與消費者。

為共享演算法調整屬性

以下應用程式代碼衡量兩種現有框架類型所代表的路徑。 兩條路徑的長度均為: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

函式庫的來源定義Structural.Adapters了 和 Structural.Algorithms。 介面卡在擁有不同原生 API 的類型上,暴露相同的座標屬性:

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

該演算法要求一個 Coordinates 性質,回傳一對浮點數值:

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))

在每次具體呼叫時,編譯器會選擇一個 Coordinates 成員,稱為約束 的見證。 內嵌演算法接受內在性質或擴展性質;空路徑與單點路徑長度為零。

跨集合類型共享操作

擴充約束也支援靜態成員。 函式庫模組 Structural.Adapters 利用現有 List.map 的 和 Option.map 函式定義了轉接器:

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

在函式庫模組 Structural.Algorithms 中,請求完整 Map 輸入型態的成員:

let inline map (mapping: 'T -> 'U) (source: ^Input) : ^Output =
    (^Input :
        (static member Map: ^Input * ('T -> 'U) -> ^Output)
        (source, mapping))

應用程式接著可以共享轉換、變更元素類型,並在巢式集合中組合映射:

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]

編譯器檢查的是成員名稱和類型,而非介面或映射規則。 這不是高階多態性:沒有任何參數代表像 F<_>這樣的容器構造子。

運算子擴展

操作員也可以使用相同的機制。 例如,定義一個列表與選項的映射運算子:

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))

消費者打開 StructuralOperators 並使用運算子:

open StructuralOperators

let incremented: int list = [1; 2; 3] |>> ((+) 10)
let length: int option = Some "hello" |>> String.length
// [11; 12; 13] and Some 5

內建的原始運算子解會保留匹配類型的優先權。 擴充不會提供任意的混合數值擴寬,也不會取代所有內建運算子。

F# 11.0 模式可接受一般的擴充運算子宣告,但無法作為 SRTP 解決方案使用。 操作員專案使用預覽功能。

元組運算子與上下文

一組觀測值與一對獨立累積結果可參與同一組合協議。 以下消費者在開啟函式庫的轉接器後定義其通用演算法:

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"])

這些函式庫擴充賦予運算子對整數、列表與成對的意義:

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 需要一個非空的列表,因為它使用 List.reduce。 操作會保留清單順序;編譯器不會強制執行關聯性,也不會為空輸入提供身份。

原生標頭 type ('T1 * 'T2) with ,以及 type struct ('T1 * 'T2) with 擴充 CLR 參考與值元組。 兩者皆為 F# 11.2 功能,支援第二至第七元素;較大的頭會產生 FS3915。

元組也可以同時攜帶上下文與值。 以下運算子在應用情境函數時附加一條軌跡:

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)

元組點實例呼叫和原生函式型標頭(例如 不支援)。type ('a -> 'b) with

作曲庫和消費者都使用預覽。 請保留 combineAll 消費者:將該約簡匯出後,元組 ++可產生 FS0193。 分別的路徑與映射範例會匯出它們的演算法。

回歸導向構造

對於在容器中引入純值的操作,輸出型別可以指定應該構造該值的成員:

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"

消費者的結果註解選擇輸出型別的 Return 副檔名。 僅靠輸入無法選擇容器。

每個容器供應其自身成員。 對於單一類型僅回傳的超載,請使用獨立 AllowOverloadOnReturnType 屬性。

編譯函式庫與使用者

語言設定是依專案而定,而非透過參考傳遞。 以下矩陣使用 .NET 11.0.100 SDK 中 F# 11.2 功能的設定:

函式庫實作 消費者使用 預覽需求
宣告原生元組擴充標頭。 解析其延伸運算子。 兩者都需要預覽;新的標頭語法本身是預覽。
定義普通擴展並匯出未解決的內聯成員約束。 提供具體型別,其擴展能解決該限制。 消費者需要預覽。 函式庫可以使用 F# 11.0。
內部解決擴充約束,並暴露一般或已解析的內嵌 API。 呼叫該 API,無需進一步解析擴充功能。 圖書館需要預覽;消費者可以使用 F# 11.0。
解決擴充並匯出限制,消費者也透過擴充解決。 解決剩餘的限制。 兩者都需要預覽。

路徑與映射 函式庫 使用 11.0;其 使用者 使用 preview。 從消費者目錄中執行 dotnet run 支援的 F# 11 SDK。

跨組件的延伸範圍

使用公開擴充功能,並在定義依賴的內聯函式前開啟它們的模組。 對於匯入的未解決約束,也開啟消費者的轉接器;製作人的 open 聲明不會轉移。 已經解決的 API 不要求消費者重新開啟轉接器。

推論與簽名

F# 11.2 解析度延後了 F# 11.0 模式先前所做的部分選擇。 考慮一個由 DateTime 接收器定義的內嵌輔助器:

let inline shift (date: System.DateTime) offset = date + offset

在 F# 11.0 模式下,此形式可推導 DateTime -> TimeSpan -> DateTime。 啟用 F# 11.2 擴展解析度後,可保留對 (DateTime or ^Offset) : (static member (+) : DateTime * ^Offset -> ^Result)的約束。 消費者接著可以選擇一個公開、範圍內的延伸,以取得不同的偏移類型。

消費者無法恢復 F# 11.0 函式庫中遺失的一般性。 檢查匯出簽名;製作人可能還需要啟用 F# 11.2 推論。

預覽限制與故障排除

  • 對於 FS0001 或 FS0193,請檢查語言設定、公開可見性及適配範圍。
  • 如果後續的生產檔解析了路徑範例, Complex程式碼產生可能會因 FS0971, Undefined value 'get_Coordinates'而失敗。 保留該解析度及其定義,或放在獨立消費者中。
  • 競爭的轉接器會使成員選擇變得模糊。

也請參閱