Parameterization / retrieval of cases of discriminatory union

I am currently working in a game and I use Event / Observables a lot, one thing I came across was to eliminate some redundant code, and I did not find a way to do this. To explain this, suppose we have the following DUs and the DUs observed for this.

type Health = | Healed | Damaged | Died | Revived let health = Event<Health>() let pub = health.Publish 

I have many such structures. Having all the Health messages grouped together is useful and necessary in some situations, but in some situations I only deal with a special message. Since this is still often necessary, I use Observable.choose to separate these messages. I then have code like this.

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

Writing such code is actually quite repetitive and annoying. I have many of these types and messages. Thus, one “rule” of functional programming is “Parametrize all the things”. So I wrote an only function to just help me.

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

With this feature in place, the code now becomes much shorter and less annoying to write.

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

EDIT: It is important to note. healed , damaged , died , revived now have the type IObservable<unit> not IObservable<Health> . The idea is not only to separate messages. This can be easily achieved with Observable.filter . The idea is that data for each case is additionally retrieved. For a DU case that does not carry any additional data, this is easy, since I need to write Some () in the Observable.choose function.

But this only works until different cases in the DU expect additional values. Unlucky, I also have many cases that contain additional information. For example, instead of healed or damaged , I have HealedBy of int . Thus, the message also contains additional information about how much something was healed. In this case, I am doing something like this.

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

But I really want to write something like this

 let healedBy = pub |> onlyWith HealeadBy 

I expect to get an Observable<int> . And I did not find a way to do this. I can not write a function like only above. because when I try to evaluate msg inside the pattern match, it is just treated as a variable for Pattern Match all cases. I can’t say something like: "Case coincidence inside a variable."

I can check if a variable has any particular case. I can do if x = HealedBy then , but after that I cannot extract any data from x . What I really need is something like an “insecure” option, for example, provide it with optional.Value . Is there a way to implement such a "onlyWith" function to delete a template?


EDIT: The idea is not only to separate the various messages. This can be achieved using Observable.filter . Here healedBy is of type IObservable<int> NOT IObservable<Health> . The big idea is to separate the AND messages and the data distractions that it conducts along the AND without a special pattern. I can already split and extract it at a time using Observable.choose . As long as the case has no additional data, I can use the only function to get rid of the template.

But as soon as the case has additional data, I will return to writing the repeating Observable.choose function and repeat all the pattern matching. I currently have such code.

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

And I have such things for a lot of posts and different types. But the only thing that changes is the “X” in it. So I obviously want to parameterize "X", so I don’t need to write the whole construct over and over again. At best, it should be

 let observ = anyObservable |> onlyWith CaseIWantToSeparate 

But the new Observable refers to the type of specific case that I shared. Not the type of DU itself.

+6
source share
4 answers

The behavior you are looking for does not exist, it works fine in your first example, because you can always return unit option sequentially.

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

Note that this is of type: 'a -> IObservable<'a> -> IObservable<unit>

Now imagine that to create a clear example, I am defining a new DU that can contain several types:

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

Imagine that, as a mental exercise, I am trying to define some general function that does the same thing as above. What could be his signature type?

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

??? cannot be one of the specific types above, because the types are all different, and cannot be a general type for the same reason.

Since it is not possible to create an understandable type signature for this function, this is a pretty strong consequence that it is not a way to do this.

The core of the problem you are facing is that you cannot select the type of return value at runtime, returning a data type that may have several different possible, but specific cases, exactly the problem you are solving the problem with. / p>

Thus, your only option is to explicitly handle each case, you already know or have seen several options for how to do this. Personally, I see nothing wrong with defining some helper functions:

 let tryGetHealedValue = function |HealedBy hp -> Some hp |None -> None let tryGetDamagedValue = function |DamagedBy dmg -> Some dmg |None -> None 
+3
source

It doesn't look like you can get your onlyWith function without making significant changes elsewhere. You cannot generalize the function that you pass for the HealedBy case while in the type system (I suppose you could fool with reflection).

One thing that seems like a good idea is to introduce a wrapper for the Healed type instead of the HealedBy type:

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

and then you can use the onlyWith function as follows:

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

I think you can even go even further while you're on it, and parameterize your type with both a label and sum types to make it truly generic:

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

Change To reboot, you save this DU:

 type Health = | Healed | Damaged | Died | Revived 

Then you make your health event - another - use the Quantified type:

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

You can trigger an event with messages like { label = Healed; amount = 10 } { label = Healed; amount = 10 } or { label = Died; amount = 0 } { label = Died; amount = 0 } . And you can use the only and onlyWith to filter and project the flow of events on the IObservable<unit> and IObservable<int> respectively, without introducing any template filtering functions.

  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 

One shortcut is enough to distinguish between records containing "Healed" and "Died", you no longer need to go around the payload that you would have in your old "HealedBy" case. In addition, if you now add DU Mana or Stamina , you can reuse the same common functions with the type Quantified<Mana, float> , etc.

Does that make sense to you?

It may be a little far-fetched compared to a simple DU with HealedBy and DamagedBy, but it optimizes the use case you use.

+1
source

In these situations, the usual way is to define predicates for cases, and then use them to filter:

 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 

UPDATE
Based on your comment: if you want to not only filter messages, but also expand your data, you can define similar option -typed functions and use them with 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> 
+1
source

You can use reflection to do this, I think. This can be pretty slow:

 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 
+1
source

All Articles