Is it possible to pass discriminated union tags as arguments?

为君一笑 提交于 2019-11-29 14:24:25

问题


Is it possible to pass the type of a discriminated union tag to another function so it can use it for pattern matching?

Non working example of what I mean:

type Animal = Pig of string | Cow of string | Fish of string

let animals = [Pig "Mike"; Pig "Sarah"; Fish "Eve"; Cow "Laura"; Pig "John"]

let rec filterAnimals animalType animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals animalType (List.tail animals)
        match List.head animals with
        |animalType animal -> animal::rest // <- this doesn't work
        |_ -> rest

printfn "%A" (filterAnimals Pig animals)

回答1:


Discriminated unions works best if there is no semantic overlap between the cases.

In your example, each case contains the same component with the same meaning, a string indicating "the name of the animal". But that's a semantic overlap! The discriminating union will then force you to make distinctions you don't want to: You don't want to be forced to discriminate between the "name of a pig" and the "name of a cow"; you just want to think of "the name of an animal".

Let's make a type that fits better:

type Animal = Pig  | Cow  | Fish
type Pet = Animal * string

let animals = [(Pig, "Mike"); (Fish, "Eve"); (Pig, "Romeo")

With that type, filtering out non-Pigs is a one-liner:

animals |> List.filter (fst >> (=) Pig) 

If not every animal has a name, use an option type:

type Pet = Animal * string option

You would use a discriminated union for your animals if you knew that, say, every Pig has a name, but no Fish does: those cases have no overlap.




回答2:


You can change the filterAnimals function to take a Partial Active Pattern as input:

let rec filterAnimals (|IsAnimalType|_|) animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals (|IsAnimalType|_|) (List.tail animals)
        match List.head animals with
        | IsAnimalType animal -> animal::rest
        | _ -> rest

Then you can define an Active Partial Pattern for pigs:

let (|IsPig|_|) candidate =
    match candidate with
    | Pig(_) -> Some candidate
    | _ -> None

And you can call the function like this (FSI example):

> filterAnimals (|IsPig|_|) animals;;
val it : Animal list = [Pig "Mike"; Pig "Sarah"; Pig "John"]

Actually, you can reduce the Partial Active Pattern like this:

let (|IsPig|_|) = function | Pig(x) -> Some(Pig(x)) | _ -> None

And it turns out that you can even inline them:

> filterAnimals (function | Pig(x) -> Some(Pig(x)) | _ -> None) animals;;
val it : Animal list = [Pig "Mike"; Pig "Sarah"; Pig "John"]
> filterAnimals (function | Fish(x) -> Some(Fish(x)) | _ -> None) animals;;
val it : Animal list = [Fish "Eve"]
> filterAnimals (function | Cow(x) -> Some(Cow(x)) | _ -> None) animals;;
val it : Animal list = [Cow "Laura"]



回答3:


Just for completeness, I'll list this solution.
If you quoted your input, you'd be able to reason about the names of the tags:

open Microsoft.FSharp.Quotations

type Animal = Pig of string | Cow of string | Fish of string

let isAnimal (animalType : Expr) (animal : Expr) =
    match animal with
        | NewUnionCase(at, _) ->
            match animalType with
                | Lambda (_, NewUnionCase (aatt, _)) -> aatt.Name = at.Name
                | _ -> false
        | _ -> false

let animal = <@ Pig "Mike" @>
isAnimal <@ Pig @> animal  // True
isAnimal <@ Cow @> animal  // False

This is admittedly quite verbose though, and it would become even more so if you wanted to quote a list instead of a single value.

A slightly different version, where we quote only the animal type, would let you easily filter lists of animals, as you need (at the price of some questionable comparison of strings):

open Microsoft.FSharp.Quotations

type Animal = Pig of string | Cow of string | Fish of string

let isAnimal (animalType : Expr) animal =
    match animalType with
        | Lambda (_, NewUnionCase (aatt, _)) -> animal.ToString().EndsWith(aatt.Name)
        | _ -> false

let animal = Pig "Mike"  // No quote now, so we can process Animal lists easily
isAnimal <@ Pig @> animal  // True
isAnimal <@ Cow @> animal  // False

let animals = [Pig "Mike"; Pig "Sarah"; Fish "Eve"; Cow "Laura"; Pig "John"]
let pigs = animals |> List.filter (isAnimal <@ Pig @>)

The latter version is not really superior to passing the tag name as a string.




回答4:


No, it's not possible to pass just the tag, in order to be able to treat separately the tag and the string you can define them like this:

type AnimalType = Pig  | Cow  | Fish
type Animal = Animal of AnimalType * string

let animals = [Animal (Pig, "Mike"); Animal (Pig, "Sarah"); Animal (Fish, "Eve"); Animal (Cow, "Laura"); Animal (Pig, "John")]

let rec filterAnimals animalType animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals animalType (List.tail animals)
        match List.head animals with
        | Animal (x, animal) when x = animalType -> animal::restwork
        |_ -> rest

printfn "%A" (filterAnimals Pig animals)

Alternatively you can use just a tuple of AnimalType * string

UPDATE

Regarding your question in the comments about what happens if the structure is not always the same, there's a trick you can use: you can compare the type of two Discriminated Unions since each tag is compiled to a different sub-class.

type Animal = 
    | Person of string * string 
    | Pig of string 
    | Cow of string 
    | Fish of string

let animals = [Pig "Mike"; Pig "Sarah"; Fish "Eve"; Cow "Laura"; Pig "John"]

let rec filterAnimals animalType animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals animalType (List.tail animals)
        match List.head animals with
        | x when animalType.GetType() = x.GetType() -> x::rest
        |_ -> rest

printfn "%A" (filterAnimals (Pig "") animals)

But before going this way, you should think if you really need to model your problem like this.

Even if you decide to go with this structure I would rather use the built-in filter function, see the solution proposed by @polkduran.




回答5:


This does not answer your question directly but suggests an alternative way to achieve what you want. You could filter your list using the existing high order function List.filter and pattern matching:

let pigs = animals |> List.filter (function |Pig(_)-> true |_->false ) 

I think this is a more idiomatic approach: you filter your list using a pattern, in this case you filter your animals keeping only those who satisfy the pattern Pig(_).



来源:https://stackoverflow.com/questions/22657000/is-it-possible-to-pass-discriminated-union-tags-as-arguments

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!