Parameterizing/Extracting 受歧视的联盟案例

Parameterizing/Extracting Discriminated Union cases

目前我正在做一个游戏并且经常使用 Event/Observables,我 运行 做的一件事是消除一些冗余代码,但我没有找到一种方法来做到这一点。为了解释它,让我们假设我们有跟随 DU 和这个 DU 的 Observable。

type Health =
    | Healed
    | Damaged
    | Died
    | Revived

let health = Event<Health>()
let pub    = health.Publish

我有很多这种结构。将所有 "Health" 消息组合在一起在某些情况下很有帮助并且需要,但在某些情况下我只关心一个特殊的消息。因为仍然经常需要我使用 Observable.choose 来分隔这些消息。然后我有这样的代码。

let healed = pub |> Observable.choose (function 
    | Healed -> Some ()
    | _      -> None
)

let damaged = pub |> Observable.choose (function
    | Damaged -> Some ()
    | _       -> None
)

写这种代码实际上是非常重复和烦人的。我有很多这样的类型和消息。所以一个 "rule" 的函数式编程是 "Parametrize all the things"。所以我写了一个函数 only 来帮助我。

let only msg pub = pub |> Observable.choose (function
    | x when x = msg -> Some ()
    | _              -> None
)

有了这样的功能,现在代码变得更短,写起来也不那么烦人了。

let healed  = pub |> only Healed
let damaged = pub |> only Damaged
let died    = pub |> only Died
let revived = pub |> only Revived

编辑: 需要注意的重要事项。 healeddamageddiedrevived 现在属于 IObservable<unit> 而非 IObservable<Health> 类型。这个想法不仅仅是分离消息。这可以通过 Observable.filter 轻松实现。这个想法是提取每个案例的数据。对于不携带任何附加数据的 DU 情况,这很容易,因为我只需要在 Observable.choose 函数中写入 Some ()

但这只适用于 DU 中的不同情况不期望额外的值。不幸的是,我还有很多带有附加信息的案例。例如,我有 HealedBy of int 而不是 HealedDamaged。因此,一条消息还包含额外的东西被治愈了多少。在这种情况下,我正在做的就是这样。

let healedBy = pub |> Observable.choose (function
    | HealedBy x -> Some x
    | _          -> None
)

但我真正想要的是这样写

let healedBy = pub |> onlyWith HealeadBy

我期待的是 Observable<int>。而且我没有找到任何方法。我不能写像上面 only 这样的函数。因为当我尝试在模式匹配中评估 msg 时,它只是被视为模式匹配所有情况的变量。我不能这样说:"Match on the case inside the variable."

我可以检查变量是否属于某些特定情况。我可以 if x = HealedBy then 但之后,我无法从 x 中提取任何类型的数据。我真正需要的是类似 "unsecure" 的提取选项,例如为它提供 optional.Value。有没有什么方法可以实现这样的 "onlyWith" 功能来删除样板文件?


编辑: 这个想法不仅仅是分离不同的消息。这可以通过 Observable.filter 来实现。这里 healedBy 不再是 IObservable<int> 类型,不再是 IObservable<Health> 类型。主要的想法是分离消息 AND 提取它携带的数据 AND 这样做没有太多样板。我现在已经可以用 Observable.choose 一次性分离和提取它了。只要案例没有任何其他数据,我就可以使用 only 函数来摆脱样板文件。

但是一旦案例有额外的数据,我就会重新编写重复的 Observable.Choose 函数并再次执行所有模式匹配。事情是目前我有这样的代码。

let observ = pub |> Observable.choose (function 
    | X (a) -> Some a
    | _     -> None
)

我有很多消息和不同类型的东西。但唯一改变的是其中的 "X" 。所以我显然想参数化 "X" 这样我就不必一次又一次地编写整个构造。充其量应该是

let observ = anyObservable |> onlyWith CaseIWantToSeparate

但是新的 Observable 是我分离的特定案例的类型。不是 DU 本身的类型。

如果不在其他地方进行一些重大更改,您似乎无法获得 onlyWith 功能。在保持类型系统的同时,您无法真正概括为 HealedBy 案例传递的函数(我想您可以通过反射作弊)。

似乎是个好主意的一件事是为 Healed 类型引入包装器而不是 HealedBy 类型:

type QuantifiedHealth<'a> = { health: Health; amount: 'a }

然后你可以有一个像这样的 onlyWith 函数:

let onlyWith msg pub =
    pub |> Observable.choose (function
        | { health = health; amount = amount } when health = msg -> Some amount
        | _ -> None)

我猜你甚至可以更进一步,通过标签和数量类型参数化你的类型,使其真正通用:

type Quantified<'label,'amount> = { label: 'label; amount: 'amount }

编辑:重申一下,你保留这个 DU:

type Health =
    | Healed
    | Damaged
    | Died
    | Revived

然后你创建你的健康事件 - 仍然是一个 - 使用Quantified类型:

let health = Event<Quantified<Health, int>>()
let pub    = health.Publish

您可以使用 { label = Healed; amount = 10 }{ label = Died; amount = 0 } 等消息触发事件。并且可以使用 onlyonlyWith 函数将事件流分别过滤和投影到 IObservable<unit>IObservable<int>,而无需引入任何样板过滤函数。

 let healed  : IObservable<int>  = pub |> onlyWith Healed
 let damaged : IObservable<int>  = pub |> onlyWith Damaged
 let died    : IObservable<unit> = pub |> only Died
 let revived : IObservable<unit> = pub |> only Revived

标签本身就足以区分代表 "Healed" 和 "Died" 案例的记录,您不再需要绕过旧 "HealedBy" 案例中的负载。此外,如果您现在添加 ManaStamina DU,则可以重用具有 Quantified<Mana, float> 类型等的相同通用函数

你觉得这有意义吗?

可以说,与具有 "HealedBy" 和 "DamagedBy" 的简单 DU 相比,它有点做作,但它确实优化了您关心的用例。

在这些情况下通常的做法是为案例定义谓词,然后使用它们进行过滤:

type Health = | Healed | Damaged | Died | Revived

let isHealed = function | Healed -> true | _ -> false
let isDamaged = function | Damaged -> true | _ -> false
let isDied = function | Died -> true | _ -> false
let isRevived = function | Revived -> true | _ -> false

let onlyHealed = pub |> Observable.filter isHealed

更新
根据您的评论:如果您不仅要过滤消息,还要打开它们的数据,您可以定义类似的 option 类型的函数并将它们与 Observable.choose:

一起使用
type Health = | HealedBy of int | DamagedBy of int | Died | Revived

let getHealed = function | HealedBy x -> Some x | _ -> None
let getDamaged = function | DamagedBy x -> Some x | _ -> None
let getDied = function | Died -> Some() | _ -> None
let getRevived = function | Revived -> Some() | _ -> None

let onlyHealed = pub |> Observable.choose getHealed  // : Observable<int>
let onlyDamaged = pub |> Observable.choose getDamaged  // : Observable<int>
let onlyDied = pub |> Observable.choose getDied  // : Observable<unit>

我认为你可以使用反射来做到这一点。这可能会很慢:

open Microsoft.FSharp.Reflection

type Health =
    | Healed of int
    | Damaged  of int
    | Died 
    | Revived 

let GetUnionCaseInfo (x:'a) = 
    match FSharpValue.GetUnionFields(x, typeof<'a>) with
    | case, [||] -> (case.Name, null )
    | case, value -> (case.Name, value.[0] )


let health = Event<Health>()
let pub    = health.Publish

let only msg pub = pub |> Observable.choose (function
    | x when x = msg -> Some(snd (GetUnionCaseInfo(x)))
    | x when fst (GetUnionCaseInfo(x)) = fst (GetUnionCaseInfo(msg)) 
                    -> Some(snd (GetUnionCaseInfo(x)))
    | _              -> None
)

let healed  = pub |> only (Healed 0)
let damaged = pub |> only (Damaged 0)
let died    = pub |> only Died
let revived = pub |> only Revived

[<EntryPoint>]
let main argv = 
    let healing = Healed 50
    let damage = Damaged 100
    let die = Died
    let revive = Revived

    healed.Add (fun i ->
            printfn "We healed for %A." i)

    damaged.Add (fun i ->
            printfn "We took %A damage." i)

    died.Add (fun i ->
            printfn "We died.")

    revived.Add (fun i ->
            printfn "We revived.")

    health.Trigger(damage)
    //We took 100 damage.
    health.Trigger(die)
    //We died.
    health.Trigger(healing)
    //We healed for 50.    
    health.Trigger(revive)
    //We revived.

    0 // return an integer exit code

您正在寻找的行为似乎不存在,它在您的第一个示例中运行良好,因为您始终可以始终如一地 return unit option.

let only msg pub = 
    pub |> Observable.choose (function
        | x when x = msg -> Some ()
        | _              -> None)

注意它的类型:'a -> IObservable<'a> -> IObservable<unit>

现在,让我们想象一下,为了创建一个清晰的示例,我定义了一些可以包含多种类型的新 DU:

type Example =
    |String of string
    |Int of int
    |Float of float

想象一下,作为一种思维练习,我现在尝试定义一些与上述功能相同的通用函数。它的类型签名可能是什么?

Example -> IObservable<Example> -> IObservable<???>

??? 不能是上面的任何一个具体类型,因为类型都是不同的,也不能是通用类型,同样的原因。

因为不可能为这个函数想出一个合理的类型签名,这就强烈暗示这不是实现它的方法。

您遇到的问题的核心是您无法在运行时决定 return 类型,returning 数据类型可以是几种不同的可能但已定义的情况正是歧视工会帮你解决的问题

因此,您唯一的选择是明确处理每个案例,您已经知道或已经看到了如何执行此操作的多个选项。就个人而言,我认为定义一些要使用的辅助函数没有什么可怕的:

let tryGetHealedValue = function
    |HealedBy hp -> Some hp
    |None -> None

let tryGetDamagedValue = function
    |DamagedBy dmg -> Some dmg
    |None -> None