离散联合上的 F# 高阶函数

F# higher-order functions on discrete unions

我正在尝试用 F# 编写一个 REST 客户端,它使用强类型值来定义资源 "signatures"。我决定使用离散联合将签名表示为可选参数列表。我计划提供一种通用方法,将参数值转换为表示将用于创建请求的 key/value 对的元组列表。这是一个学习练习,所以我正在尝试使用 idomatic F#。

我被卡住了,试图定义两个具有相似签名的不同离散联合。有没有办法让我在运行时动态 select 正确的模式匹配函数?

type A =
    | First of string
    | Second of string

type B =
    | First of string
    | Second of string
    | Third of string

let createA f s =
    [A.First f; A.Second s;]

let createB f s t =
    [B.First f; B.Second s; B.Third t]

let toParamA elem =
    match elem with
    | A.First f -> "First", f
    | A.Second s -> "Second", s

let toParamB elem =
    match elem with
    | B.First f -> "First", f
    | B.Second s -> "Second", s
    | B.Third t -> "Third", t

let rec toParam f state args =
    match args with
    | [] -> state
    | head::tail -> 
        let state' = (f head)::state
        toParam f state' tail

let argA = createA "one" "two"
let argB = createB "one" "two" "three"

let resultA = argA |> toParam toParamA []
let resultB = argB |> toParam toParamB []

结果目前是正确的,我只是对 API 不满意:

val resultA : (string * string) list = [("Second", "two"); ("First", "one")]
val resultB : (string * string) list = [("Third", "three"); ("Second", "two"); ("First", "one")]

更新:

问题是我希望通话看起来像什么?

let resultA = argA |> toParam []

然后toParam会判断是调用toParamA还是调用toParamB

我想我已经意识到我原来的方法适用于当前情况。但是,我仍然有兴趣知道我的伪代码是否可行。

我认为最普通的 F# 方式是明确说明您正在构建参数列表的 API 方法:

type ApiArgs = ApiA of A list | ApiB of B list

然后您可以像这样合并 toParamAtoParamB 函数:

let toParam = function
| ApiA args ->
    let toParamA = function
    | A.First x -> "First", x
    | A.Second x -> "Second", x

    List.map toParamA args 
| ApiB args ->
    let toParamB = function
    | B.First x -> "First", x
    | B.Second x -> "Second", x
    | B.Third x -> "Third", x

    List.map toParamB args 

我在这里看到两种改进的可能性。首先,代码过于重复和乏味。您可能可以为您的 API 使用类型提供程序生成代码,或者在 运行 时使用反射来进行转换。

其次,将 AB 转换为 (string * string) list 的多态行为发生在 运行 时,但我认为您可以在编译时完成它:

type X = X with
    static member ($) (_, args : A list) =
        let toParamA = function
        | A.First x -> "First", x
        | A.Second x -> "Second", x

        List.map toParamA args

    static member ($) (_, args : B list) =
        let toParamB = function
        | B.First x -> "First", x
        | B.Second x -> "Second", x
        | B.Third x -> "Third", x

        List.map toParamB args

let inline toParam' args = X $ args

如果您检查 toParam' 的推断类型,它看起来类似于:

val inline toParam' :
  args: ^a ->  ^_arg3
    when (X or  ^a) : (static member ( $ ) : X *  ^a ->  ^_arg3)

^a 符号是所谓的 "hat type",阅读更多 here

然后使用不同类型的参数调用 toParam' 会产生正确的结果:

> toParam' (createA "one" "two");;
val it : (string * string) list = [("First", "one"); ("Second", "two")]
> toParam' (createB "1" "2" "3");;
val it : (string * string) list =
  [("First", "1"); ("Second", "2"); ("Third", "3")]
>

此技术在 this blog, but I believe it is an outdated way to do it. For more inspiration, take a look at these projects: FsControl, FSharpPlus, Higher 中有详细描述。