Динамическая проверка случаев объединения - PullRequest
2 голосов
/ 16 февраля 2012

Как сопоставить регистр объединений динамически в F # при наличии объявлений значений?

Неработающий код:

let myShape = Shape.Square
expect myShape Shape.Circle 

type Shape =
   | Circle of int
   | Square of int
   | Rectangle of ( int * int )

let expect someShape someUnionCase =
    if not ( someShape = someUnionCase )
    then failwith ( sprintf "Expected shape %A. Found shape %A" someShape someUnionCase )

let myShape = Shape.Square
expect myShape Shape.Circle // Here I want to compare the value types, not the values

Если в моих случаях объединения не объявлялись значения, это работает с использованием примеров создания экземпляров (а это не то, что я хочу):

let myShape = Shape.Square
expect myShape Shape.Circle 

type Shape =
   | Circle
   | Square
   | Rectangle

let expect someShape someUnionCase =
    if not ( someShape = someUnionCase )
    then failwith ( sprintf "Expected shape %A. Found shape %A" someShape someUnionCase )

let myShape = Shape.Square
expect myShape Shape.Circle // Comparing values instead of types

Ответы [ 4 ]

4 голосов
/ 16 февраля 2012

Интересно, что это можно сделать очень легко в C #, но компилятор F # не позволит вам вызывать функции, что кажется странным.

В спецификации сказано, что дискриминационный союз будет иметь (раздел 8.5.3):

Одно свойство экземпляра CLI u.Tag для каждого случая C, который выбирает или вычисляет целочисленный тег, соответствующий регистру.

Так что мы можем написать вашу ожидаемую функцию в C # тривиально

public bool expect (Shape expected, Shape actual)
{
    expected.Tag == actual.Tag;
}

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

3 голосов
/ 16 февраля 2012

Когда вы вызываете функцию expect в вашем примере, например, с. Shape.Square в качестве аргумента, вы фактически передаете ему функцию, которая принимает аргументы случая объединения и строит значение.

Динамический анализ функций довольно сложен, но вы можете вместо этого передать ему конкретные значения (например, Shape.Square(0)) и проверить, что их форма одинакова (игнорируйте числовые аргументы). Это можно сделать с помощью отражения F #. Функция FSharpValue.GetUnionFields возвращает имя падежа объекта вместе с obj[] всех аргументов (которые вы можете игнорировать):

open Microsoft.FSharp.Reflection

let expect (someShape:'T) (someUnionCase:'T) = 
  if not (FSharpType.IsUnion(typeof<'T>)) then
    failwith "Not a union!"
  else
    let info1, _ = FSharpValue.GetUnionFields(someShape, typeof<'T>)
    let info2, _ = FSharpValue.GetUnionFields(someUnionCase, typeof<'T>)
    if not (info1.Name = info2.Name) then
      failwithf "Expected shape %A. Found shape %A" info1.Name info2.Name

Если вы теперь сравниваете Square с Circle, функция выдает, но если вы сравниваете два Squares, это работает (даже если значения отличаются):

let myShape = Shape.Square(10)
expect myShape (Shape.Circle(0)) // Throws
expect myShape (Shape.Square(0)) // Fine

Если вы хотите избежать создания конкретных значений, вы также можете использовать кавычки F # и написать что-то вроде expect <@ Shape.Square @> myValue. Это немного сложнее, но, возможно, приятнее. Некоторые примеры обработки цитат можно найти здесь .

1 голос
/ 23 апреля 2012

Я использую тот же шаблон для реализации проверки типов в HLVM.Например, при индексации в массиве я проверяю, что типом выражения является массив, игнорирующий тип элемента.Но я не использую рефлексию, как предлагали другие ответы.Я просто делаю что-то вроде этого:

let eqCase = function
  | Circle _, Circle _
  | Square _, Square _
  | Rectangle _, Rectangle _ -> true
  | _ -> false

Обычно в более конкретной форме, например:

let isCircle = function
  | Circle _ -> true
  | _ -> false

Вы также можете сделать:

let (|ACircle|ASquare|ARectangle|) = function
  | Circle _ -> ACircle
  | Square _ -> ASquare
  | Rectangle _ -> ARectangle

Если вырешите пойти по пути отражения, и производительность - это проблема (отражение невероятно медленное ), затем используйте предварительно вычисленные формы:

let tagOfShape =
  Reflection.FSharpValue.PreComputeUnionTagReader typeof<Shape>

Это более чем в 60 раз быстрее, чем прямое отражение.

0 голосов
/ 31 августа 2016

ПРИМЕЧАНИЕ здесь есть оговорка.См. ОБНОВЛЕНИЕ ниже.

Похоже, что объединения объединяются как вложенные классы типа объединения (имя типа: FSI_0006+Shape+Square).Поэтому, учитывая экземпляр типа объединения, достаточно проверить тип экземпляра с помощью obj.GetType().

let expect (someShape:'T) (someUnionCase:'T) = 
    if (someShape.GetType() <> someUnionCase.GetType()) then failwith "type not compatible"

type Shape =
   | Circle of int
   | Square of int
   | Rectangle of ( int * int )

let myShape = Shape.Square 12
printfn "myShape.GetType(): %A" (myShape.GetType())
expect myShape (Shape.Circle 5)

Это выводит:

myShape.GetType(): FSI_0006+Shape+Square
System.Exception: type not compatible
   at Microsoft.FSharp.Core.Operators.FailWith[T](String message)
>    at FSI_0006.expect[T](T someShape, T someUnionCase)
   at <StartupCode$FSI_0006>.$FSI_0006.main@()
Stopped due to error

Я просто не знаю, считается ли этот подход зависимым от реализации, т. Е. Некоторые платформы / среды выполнения реализуют это по-разному, так что типы двухразличные объекты падежа одинаковы.

ОБНОВЛЕНИЕ

ОК. Я обнаружил, что вышеописанное не работает для типа объединения с делами, которые не принимают параметров.В этом случае реализация вариантов отличается, и .GetType() всегда дает тип объявления типа объединения.Приведенный ниже код демонстрирует это:

type Foo = A|B|C
type Bar = X|Y|Z of int

let getType (x:obj) = x.GetType()
let p (x:obj) = printfn "%A" x

A |> getType |> p
B |> getType |> p
C |> getType |> p
X |> getType |> p
Y |> getType |> p
Z 7 |> getType |> p

Это дает:

FSI_0004+Foo
FSI_0004+Foo
FSI_0004+Foo
FSI_0004+Bar+_X
FSI_0004+Bar+_Y
FSI_0004+Bar+Z

Более общая альтернатива, как упоминалось в другом ответе, заключалась бы в преобразовании экземпляров case в теги:

open Microsoft.FSharp.Reflection

// more general solution but slower due to reflection
let obj2Tag<'t> (x:obj) =
    FSharpValue.GetUnionFields(x, typeof<'t>) |> fst |> (fun (i: UnionCaseInfo) -> i.Tag)

[A;B;C;A] |> List.map obj2Tag<Foo> |> p
[X;Y;Z 2; Z 3; X] |> List.map obj2Tag<Bar> |> p

Это дает:

[0; 1; 2; 0]
[0; 1; 2; 2; 0]

Это должно быть значительно медленнее, если работать с большим количеством объектов, так как оно сильно зависит от отражения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...