参数化/提取受歧视的联合案例

时间:2016-02-21 14:12:07

标签: f# discriminated-union

目前我正在游戏中使用Event / Observables,我遇到的一件事是消除一些冗余代码,我没有找到办法。为了解释它,让我们假设我们跟随DU和这个DU的Observable。

type Health =
    | Healed
    | Damaged
    | Died
    | Revived

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

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

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

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

编写这种代码实际上非常重复且烦人。我有很多这些类型和消息。因此,函数式编程的一个“规则”是“参数化所有事物”。所以我写了一个函数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情况,这很容易,因为我只需要在Some ()函数中编写Observable.choose

但这只能起作用,只要DU中的不同情况不会产生额外的值。不幸的是,我也有很多带有附加信息的案例。例如,我代替HealedDamaged HealedBy of int。所以一条消息还包含了多少东西得到了治愈。在这种情况下,我正在做的是这样的事情。

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

但我真正想要的是写这样的东西

let healedBy = pub |> onlyWith HealeadBy

我期待的是获得Observable<int>。我没有找到任何方法如何做到这一点。我不能写上面的only这样的函数。因为当我尝试在模式匹配中评估msg时,它只被视为模式匹配所有情况的变量。我不能说:“匹配变量内的案例。”

我可以检查变量是否属于某种特定情况。我可以if x = HealedBy then,但之后,我无法从x中提取任何类型的数据。我真正需要的是类似“不安全”提取的选项,例如提供optional.Value。是否有任何方法可以实现这样的“onlyWith”函数来删除样板?

修改 这个想法不仅仅是分离不同的消息。这可以通过Observable.filter来实现。此处healedBy的类型为IObservable<int> NOT 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本身的类型。

4 个答案:

答案 0 :(得分:3)

您正在寻找的行为并不存在,它在您的第一个示例中运行正常,因为您始终可以始终返回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<???>

??? 不能是上述任何具体类型,因为类型都是不同的,出于同样的原因也不能是通用类型。

由于无法为此功能提供合理的类型签名,这是一个非常强烈的暗示,这不是这样做的方式。

您遇到的问题的核心是您无法在运行时决定返回类型,返回的数据类型可能有几种不同的可能但定义的情况恰好是有区别的联合帮助您解决的问题

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

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

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

答案 1 :(得分:1)

如果不在其他地方进行重大更改,您可能无法获得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

单独的标签就足以区分代表&#34; Healed&#34;并且&#34;死了&#34;在这种情况下,您不再需要在旧的&#34; HealedBy&#34;案件。此外,如果您现在添加ManaStamina DU,则可以使用Quantified<Mana, float>类型等重复使用相同的通用函数。

这对你有意义吗?

可以说它与一个简单的DU相比有点做作,而#34; HealedBy&#34;和#34; DamagedBy&#34;,但它确实优化了您关心的用例。

答案 2 :(得分:1)

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

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>

答案 3 :(得分:1)

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

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