F #: как изящно выбирать и группировать дискриминационные союзы?

Скажем, у меня есть список фигур:

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

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

Как я могу затем проверить, например. Круг существует в a? Я мог бы создать функцию для каждой фигуры

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

но я считаю, что в F # должен быть более элегантный способ, кроме необходимости определять такую ​​функцию для каждого типа фигуры. Есть?

Связанный вопрос заключается в том, как сгруппировать список фигур на основе типов фигур:

a |> seq.groupBy( <shapetype? >)

Ответы

Ответ 1

вы можете комбинировать отражение F # с котировками для получения общего решения

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

let isUnionCase (c : Expr<_ -> 'T>)  = 
    match c with
    | Lambdas (_, 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

Ответ 2

Если вас интересуют разные категории фигур, тогда имеет смысл определить другой тип, который их точно фиксирует:

type shapeCategory = Circular | Rectangular

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

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

a |> Seq.groupBy(categorize)

Изменить - как предложил Брайан, вы можете использовать активные шаблоны вместо нового типа. Это очень похоже на ваши примеры, но будет лучше распространяться на более сложные шаблоны, в то время как вышеприведенный подход может быть лучше, если вы часто используете код с категориями, и вы хотите использовать для них симпатичный тип объединения, а не тип выбора.

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)

Ответ 3

Вы можете использовать библиотеку отражения F #, чтобы получить тег значения:

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

a |> Seq.groupBy getTag

Ответ 4

Я хочу добавить еще одно решение, которое работает с котировками для каждого случая объединения, на основе предоставленного одного desco. Вот оно:

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."

Определенный таким образом isUnionCase работает, как показал desco, но даже в случаях объединения, которые пусты или имеют более одного значения. Вы также можете ввести кортеж из разделенных запятыми случаев объединения. Рассмотрим это:

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"

Первый isUnionCase я предоставил только для проверки одного случая. Позднее я добавил проверку выражения для NewTuple и подумал, что вам это может понравиться. Просто убедитесь, что если вы измените код, прекомпьютеры все еще работают, поэтому iucs определяется вне возвращаемой анонимной функции.

Ответ 5

Более элегантным решением может быть следующее:

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

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

Однако я не очень доволен этим, потому что вам нужно создать экземпляр дискриминационного объединения, чтобы он работал.

Группировка по типу формы может работать аналогичным образом.