目前我正在游戏中使用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
修改
重要的是要注意。 healed
,damaged
,died
,revived
现在属于IObservable<unit>
而非IObservable<Health>
。这个想法不仅仅是分开信息。这可以通过Observable.filter
轻松实现。这个想法是提取每个案例的数据。对于不携带任何其他数据的DU情况,这很容易,因为我只需要在Some ()
函数中编写Observable.choose
。
但这只能起作用,只要DU中的不同情况不会产生额外的值。不幸的是,我也有很多带有附加信息的案例。例如,我代替Healed
或Damaged
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本身的类型。
答案 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 }
等消息触发事件。您可以使用only
和onlyWith
函数分别过滤事件流并将其投影到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;案件。此外,如果您现在添加Mana
或Stamina
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