17

Say I have a list of shapes:

type shape = 
| Circle of float
| Rectangle of float * float

let a = [ Circle 5.0; Rectangle (4.0, 6.0)]

How can I then test e.g. a Circle exists in a? I could create a function for each shape

let isCircle s = 
    match s with
    | Circle -> true
    | _ -> false
List.exists isCircle a

but I feel there must be a more elegant way in F#, other than having to define such a function for each shape type. Is there?

Related question is how to group a list of shapes, based on shape types:

a |> seq.groupBy( <shapetype? >)
Emile
  • 2,200
  • 27
  • 36
  • 2
    (slightly OT) This reminds me, high time that [code highlighting is supported for F#](http://meta.stackexchange.com/questions/58934/hight-time-for-code-highlighting-f-snippets) (!) – Abel Jul 29 '10 at 14:06
  • See http://meta.stackexchange.com/questions/981/syntax-highlighting-hints there is no language-specific highlighting on SO. – Brian Jul 29 '10 at 18:30

5 Answers5

17

If you're interested in the different categories of shapes, then it makes sense to define another type that exactly captures them:

type shapeCategory = Circular | Rectangular

let categorize = function
    | Circle _ -> Circular
    | Rectangle _ -> Rectangular

List.exists ((=) Circular) (List.map categorize a)

a |> Seq.groupBy(categorize)

Edit - as suggested by Brian, you can alternatively use active patterns instead of a new type. It works out pretty similarly for your examples, but would extend better to more complicated patterns, while the approach above may be better if you're code often works with the categories, and you want a nice union type for them instead of a Choice type.

let (|Circular|Rectangular|) = function 
    | Circle _ -> Circular
    | Rectangle _ -> Rectangular 

List.exists (function Circular -> true | _ -> false) a

let categorize : shape -> Choice<unit, unit> =  (|Circular|Rectangular|) 
a |> Seq.groupBy(categorize)
RD1
  • 3,305
  • 19
  • 28
  • 7
    Alternatively, an Active Pattern. – Brian Jul 29 '10 at 15:53
  • Could you give an example how that would look like? – Emile Jul 29 '10 at 19:10
  • 1
    I've added a version with Active Patterns, and brief comparison. – RD1 Jul 30 '10 at 03:00
  • @Brian: Too bad `Circle` and `Rectangle` can't be interpreted as active patterns by default in a pattern-matching context as they are functions too. Would be a nice and logical addition to F# ... – Dario Jul 30 '10 at 07:03
  • @Dario: Circle and Rectangle already have a meaning in patterns, so I don't think it would be logical to give them a another one. It's very natural to create separate names for Circular and Rectangular: it's a different concept from `Circle r` and `Rectangle w h`. – RD1 Jul 30 '10 at 09:43
  • @RD1: I think you got me wrong. I don't intend to give them another meaning in patterns but that their current meaning can be referred to as a first-class value (i.e. ADT matching recognizers aren't different from user-defined active pattern) – Dario Jul 30 '10 at 16:12
  • @Dario You're right, I don't understand. :-) Do you mean so we can define: `let categorize = (|Circle|Rectangle|)` ? If so I don't see how this helps - you can simulate this by defining active versions `CircleA` and `RectangleA`, but these won't forget the radius, width and height which is exactly the point of `Circular` and `Rectangular`. How were you thinking of defining something like `categorize` if constructors had default active patterns? – RD1 Jul 31 '10 at 04:05
11

you can combine F# reflection with quotations to get generic solution

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns

type Shape = 
    | Circle of float
    | Rectangle of float * float

let isUnionCase (c : Expr<_ -> 'T>)  = 
    match c with
    | Lambda (_, NewUnionCase(uci, _)) ->
        let tagReader = Microsoft.FSharp.Reflection.FSharpValue.PreComputeUnionTagReader(uci.DeclaringType)
        fun (v : 'T) -> (tagReader v) = uci.Tag
    | _ -> failwith "Invalid expression"

let a = 
    [ Circle 5.0; Rectangle (4.0, 6.0)] 
        |> List.filter (isUnionCase <@ Rectangle @>)
printf "%A" a
JSparrow
  • 131
  • 2
  • 5
desco
  • 16,642
  • 1
  • 45
  • 56
  • The isUnionCase implementation is way above my head, but it look very clever. And using it is along the lines I suspected it could look like. Thanks! – Emile Jul 30 '10 at 14:28
  • 1
    I can't seem to find the correct namespace. Microsoft.FSharp.Quotations is needed for Expr but I can't find Lambdas anywhere – Wouter Dec 22 '14 at 17:53
  • `Lambda` (note, no **s**) and `NewUnionCase` can be found under namespace `Microsoft.FSharp.Quotations.Patterns`. – Ruxo Nov 29 '15 at 01:48
10

You can use the F# reflection library to get a value's tag:

let getTag (a:'a) = 
  let (uc,_) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(a, typeof<'a>)
  uc.Name

a |> Seq.groupBy getTag
kvb
  • 54,864
  • 2
  • 91
  • 133
4

I want to add another solution that works with quotations for every union case, based on the one desco provided. Here it goes:

open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Reflection

let rec isUnionCase = function
| Lambda (_, expr) | Let (_, _, expr) -> isUnionCase expr
| NewTuple exprs -> 
    let iucs = List.map isUnionCase exprs
    fun value -> List.exists ((|>) value) iucs
| NewUnionCase (uci, _) ->
    let utr = FSharpValue.PreComputeUnionTagReader uci.DeclaringType
    box >> utr >> (=) uci.Tag
| _ -> failwith "Expression is no union case."

Defined this way, isUnionCase works like desco has shown, but even on union cases that are empty or have more than one value. You can also enter a tuple of comma-separated union cases. Consider this:

type SomeType =
| SomeCase1
| SomeCase2 of int
| SomeCase3 of int * int
| SomeCase4 of int * int * int
| SomeCase5 of int * int * int * int

let list =
    [
        SomeCase1
        SomeCase2  1
        SomeCase3 (2, 3)
        SomeCase4 (4, 5, 6)
        SomeCase5 (7, 8, 9, 10)
    ]

list 
|> List.filter (isUnionCase <@ SomeCase4 @>)
|> printfn "Matching SomeCase4: %A"

list
|> List.filter (isUnionCase <@ SomeCase3, SomeCase4 @>)
|> printfn "Matching SomeCase3 & SomeCase4: %A"

The first isUnionCase I provided only worked for single case checks. I later added the expression check for NewTuple and thought you might like it. Just make sure that if you alter the code the precomputations still work, this is why iucs is defined outside of the returned anonymous function.

Nikon the Third
  • 2,771
  • 24
  • 35
0

A more elegant solution could be the following:

let shapeExistsInList shapeType list =
    List.exists (fun e -> e.GetType() = shapeType) list

let circleExists = shapeExistsInList ((Circle 2.0).GetType()) a

However, I'm not very satisfied with this myself since you have to create an instance of the discriminated union for it to work.

Grouping by shape type could work in a similar fashion.

Ronald Wildenberg
  • 31,634
  • 14
  • 90
  • 133
  • 2
    This works in this case but not for simpler types like `type T = A | B` where the cases are *not* implemented as different types. – Mau Jul 29 '10 at 14:20
  • As an aside: That same problem -- of having to create a class instance to test a class instance -- comes up all the time. It would be interesting to see some general solutions to the problem. It might be worth starting a thread or a wiki. – TechNeilogy Jul 29 '10 at 14:24
  • There isn't some sort of typeof operator as in C#? – Ronald Wildenberg Jul 29 '10 at 14:36
  • 1
    if union has case with null as runtime representation then GetType will cause NullReferenceException. i.e None.GetType() // kaboom!!! – desco Jul 29 '10 at 17:41