Работа над неполным сопоставлением шаблонов на перечислениях

Существуют ли какие-либо творческие способы работы с .NET "слабыми" перечислениями при сопоставлении шаблонов? Я бы хотел, чтобы они функционировали аналогично DU. Вот как я сейчас справляюсь с этим. Любые лучшие идеи?

[<RequireQualifiedAccess>]
module Enum =
  let unexpected<'a, 'b, 'c when 'a : enum<'b>> (value:'a) : 'c = //'
    failwithf "Unexpected enum member: %A: %A" typeof<'a> value //'

match value with
| ConsoleSpecialKey.ControlC -> ()
| ConsoleSpecialKey.ControlBreak -> ()
| _ -> Enum.unexpected value //without this, gives "incomplete pattern matches" warning

Ответы

Ответ 1

Я думаю, что в целом это высокий порядок, именно потому, что перечисления "слабы". ConsoleSpecialKey является хорошим примером "полного" перечисления, где ControlC и ControlBreak, которые представлены 0 и 1 соответственно, являются единственными значимыми значениями, которые он может принять. Но у нас есть проблема, вы можете принудить любое целое число к ConsoleSpecialKey!:

let x = ConsoleSpecialKey.Parse(typeof<ConsoleSpecialKey>, "32") :?> ConsoleSpecialKey

Итак, шаблон, который вы дали, действительно неполный и действительно нуждается в обработке.

(), не говоря уже о более сложных перечислениях, таких как System.Reflection.BindingFlags, которые используются для битовой маскировки и все же неразличимы через информацию о типе из простых перечислений, что еще более усложняет редактирование изображения: на самом деле @ildjarn указала, что атрибут Flags используется, по договоренности, для различения полных и битмаксных перечислений, хотя компилятор не остановит вас от использования побитовых операций на перечислении, не отмеченном с этим атрибутом, снова обнаруживая слабые числа перечислений).

Но если вы работаете с определенным "полным" перечислением типа ConsoleSpecialKey и записываете этот последний незавершенный случай соответствия шаблону, все время на самом деле вас беспокоит, вы всегда можете взломать полный активный шаблон:

let (|ControlC|ControlBreak|) value =
    match value with
    | ConsoleSpecialKey.ControlC -> ControlC
    | ConsoleSpecialKey.ControlBreak -> ControlBreak
    | _ -> Enum.unexpected value

//complete
match value with
| ControlC -> ()
| ControlBreak -> ()

Однако это похоже на простое исключение случая неполного соответствия шаблону и подавление предупреждения. Я думаю, что ваше текущее решение хорошо, и вы были бы добры, чтобы придерживаться его.

Ответ 2

Следуя предложению, сделанному Стивеном в комментариях к его ответу, я получил следующее решение. Enum.unexpected выделяет недействительные значения перечисления и необработанные случаи (возможно, из-за добавления членов перечисления позже), выбрасывая FailureException в первом случае и Enum.Unhandled в последнем.

[<RequireQualifiedAccess>]
module Enum =

  open System

  exception Unhandled of string

  let isDefined<'a, 'b when 'a : enum<'b>> (value:'a) =
    let (!<) = box >> unbox >> uint64
    let typ = typeof<'a>
    if typ.IsDefined(typeof<FlagsAttribute>, false) then
      ((!< value, System.Enum.GetValues(typ) |> unbox)
      ||> Array.fold (fun n v -> n &&& ~~~(!< v)) = 0UL)
    else Enum.IsDefined(typ, value)

  let unexpected<'a, 'b, 'c when 'a : enum<'b>> (value:'a) : 'c =
    let typ = typeof<'a>
    if isDefined value then raise <| Unhandled(sprintf "Unhandled enum member: %A: %A" typ value)
    else failwithf "Undefined enum member: %A: %A" typ value

Пример

type MyEnum =
  | Case1 = 1
  | Case2 = 2

let evalEnum = function
  | MyEnum.Case1 -> printfn "OK"
  | e -> Enum.unexpected e

let test enumValue =
  try 
    evalEnum enumValue
  with
    | Failure _ -> printfn "Not an enum member"
    | Enum.Unhandled _ ->  printfn "Unhandled enum"

test MyEnum.Case1 //OK
test MyEnum.Case2 //Unhandled enum
test (enum 42)    //Not an enum member

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

Ответ 3

Я бы сказал, что это функция F #, которая заставляет вас обрабатывать неожиданные значения перечисления (поскольку их можно создавать с помощью явных преобразований, а так как дополнительные именованные значения могут быть добавлены более поздними версиями сборки). Ваш подход выглядит отлично. Другой альтернативой было бы создать активный шаблон:

let (|UnhandledEnum|) (e:'a when 'a : enum<'b>) = 
    failwithf "Unexpected enum member %A:%A" typeof<'a> e

function
| System.ConsoleSpecialKey.ControlC -> ()
| System.ConsoleSpecialKey.ControlBreak -> ()
| UnhandledEnum r -> r

Здесь процесс сопоставления с шаблоном UnhandledEnum генерирует исключение, но тип возвращаемого значения является переменным, поэтому его можно использовать с правой стороны шаблона независимо от того, какой тип возвращается из соответствия.

Ответ 4

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

Представьте себе, если по той же логике пользователи F # были вынуждены делать нулевую проверку каждый раз, когда сталкивались с ссылочным типом .Net (который может быть нулевым, как перечисление может хранить недопустимое целое число). Язык станет непригодным для использования. К счастью, перечисления не подходят так много, и мы можем заменить DU.

Изменение: эта проблема теперь решается https://github.com/dotnet/fsharp/pull/4522, при условии, что пользователи добавляют #nowarn "104" вручную. Вы получите предупреждения о несопоставленных определенных случаях DU, но не получите предупреждения, если вы охватите их все.