David Raab
David Raab

Reputation: 4488

Parameterizing/Extracting Discriminated Union cases

Currently i'm working in a game and use Event/Observables much, one thing i run into was to eliminate some redundant code, and i didn't found a way to do it. To explain it, let's assume we have following DU and an Observable for this DU.

type Health =
    | Healed
    | Damaged
    | Died
    | Revived

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

I have a lot of this kind of structures. Having all "Health" Messages grouped together is helpfull and needed in some situations, but in some situations i only care for a special Message. Because that is still often needed i use Observable.choose to separate those message. I have then code like this.

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

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

Writing this kind of code is actually pretty repetitive and annoying. I have a lot of those types and messages. So one "rule" of functional programming is "Parametrize all the things". So i wrote a function only to just help me.

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

With such a function in place, now the code becomes a lot 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: The important thing to note. healed, damaged, died, revived are now of type IObservable<unit> not IObservable<Health>. The idea is not just to separate the messages. This can be easily achieved with Observable.filter. The idea is that the the data for each case additional get extracted. For DU case that don't carry any additional data this is easy, as i only have to write Some () in the Observable.choose function.

But this only works, as long the different cases in a DU don't expect additional values. Unlucky i also have a lot of cases that carry additional information. For example instead of Healed or Damaged i have HealedBy of int. So a message also contains additional how much something got healed. What i'm doing is something like this, in this case.

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

But what i really want is to write it something like this

let healedBy = pub |> onlyWith HealeadBy

What i'm expecting is to get an Observable<int>. And i didn't found any way how to do it. I cannot write a function like only above. because when i try to evaluate msg inside a Pattern Matching then it is just seen as a variable to Pattern Match all cases. I cannot say something like: "Match on the case inside the variable."

I can check if a variable is of some specific case. I can do if x = HealedBy then but after that, i cannot extract any kind of data from x. What i'm really need is something like an "unsecure" extracting like option for example provide it with optional.Value. Does there exists any way to implement such a "onlyWith" function to remove the boilerplate?


EDIT: The idea is not just separating the different messages. This can be achieved through Observable.filter. Here healedBy is of type IObservable<int> NOT IObservable<Health> anymore. The big idea is to separate the messages AND extract the data it carries along AND doing it without much boilerplate. I already can separate and extract it in one go with Observable.choose currently. As long as a case don't have any additional data i can use the only function to get rid of the boilerplate.

But as soon a case has additional data i'm back at writing the repetitive Observable.Choose function and do all the Pattern Matching again. The thing is currently i have code like this.

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

And i have this kind of stuff for a lot of messages 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 have to write the whole construct again and again. At best it just should be

let observ = anyObservable |> onlyWith CaseIWantToSeparate

But the new Observable is of the type of the specific case i separated. Not the type of the DU itself.

Upvotes: 6

Views: 233

Answers (4)

scrwtp
scrwtp

Reputation: 13577

It doesn't feel like you can get your onlyWith function without making some significant changes elsewhere. You can't really generalize the function you pass in for the HealedBy case while staying within the type system (I suppose you could cheat with reflection).

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

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

and then you can have an onlyWith function like this:

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

I guess you can even go one step further while you're at it, and parameterize your type by both the label and the amount types to make it truly generic:

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

Edit: To reitarate, you keep this DU:

type Health =
    | Healed
    | Damaged
    | Died
    | Revived

Then you make your health event - still a single one - use the Quantified type:

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

You can trigger the event with messages like { label = Healed; amount = 10 } or { label = Died; amount = 0 }. And you can use the only and onlyWith functions to filter and project the event stream to IObservable<unit> and IObservable<int> respectively, without introducing any boilerplate 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

The label alone is enough to differentiate between records representing "Healed" and "Died" cases, you no longer need to walk around the payload you would have in your old "HealedBy" case. Also, if you now add a Mana or Stamina DU, you can reuse the same generic functions with Quantified<Mana, float> type etc.

Does this make sense to you?

Arguably it's slightly contrived compared to a simple DU with "HealedBy" and "DamagedBy", but it does optimize the use case that you care for.

Upvotes: 1

TheInnerLight
TheInnerLight

Reputation: 12184

The behaviour it appears you are looking for doesn't exist, it works fine in your first example because you can always consistently return a unit option.

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

Notice that this has type: 'a -> IObservable<'a> -> IObservable<unit>

Now, let's imagine for the sake of creating a clear example that I define some new DU that can contain several types:

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

Imagine, as a thought exercise, I now try to define some general function that does the same as the above. What might its type signature be?

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

??? can't be any of the concrete types above because the types are all different, nor can it be a generic type for the same reason.

Since it's impossible to come up with a sensible type signature for this function, that's a pretty strong implication that this isn't the way to do it.

The core of the problem you are experiencing is that you can't decide on a return type at runtime, returning a data type that can be of several different possible but defined cases is precisely the problem discriminated unions help you solve.

As such, your only option is to explicitly handle each case, you already know or have seen several options for how to do this. Personally, I don't see anything too horrible about defining some helper functions to use:

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

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

Upvotes: 3

Ringil
Ringil

Reputation: 6527

You can use reflection to do this I think. This might 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

Upvotes: 1

Fyodor Soikin
Fyodor Soikin

Reputation: 80714

The usual route in these situations is to define predicates for cases, and then use them for filtering:

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 unwrap their 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>

Upvotes: 1

Related Questions