Работа над неполным сопоставлением шаблонов на перечислениях
Существуют ли какие-либо творческие способы работы с .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, но не получите предупреждения, если вы охватите их все.