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

#f# #enums #pattern-matching

#f# #перечисления #сопоставление шаблонов

Вопрос:

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

 [<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. Кстати, вы хотите использовать failwithf , а не failwith .

Ответ №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 -> ()
  

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

Комментарии:

1. Просто чтобы уточнить, я не пытаюсь доказать, что F # должен по-разному относиться к перечислениям, но когда я хочу разрешить только определенные значения, я ищу творческие способы справиться с этим.

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

3. @StephenSwensen : Не то чтобы это уменьшало достоверность вашего ответа, стоит отметить, что перечисления с битовой маской, такие как BindingFlags , действительно различимы по информации о типе, поскольку к их определениям применяется Flags атрибут .

4. @ildjarn — ловко, я этого не знал

5. @Daniel — молодец! дополняем вашу Enum.unexpected реализацию, чтобы указать, был ли сбой вызван добавлением элементов в перечисление против неверное значение определенно приблизит вас к тому, что вам нужно.

Ответ №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 amp;amp;amp; ~~~(!< 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 вызовет исключение, но возвращаемый тип является переменным, так что его можно использовать в правой части шаблона независимо от того, какой тип возвращается в результате сопоставления.

Комментарии:

1. Как я упоминал в ответ на ответ Стивена, я думаю, что способ, которым F # обрабатывает перечисления, имеет смысл. В основном мне интересно, есть ли какой-нибудь способ избежать (скрыть) обязательного исключения при сопоставлении шаблонов.

2. Чтобы немного расширить это: возможно ли сопоставление шаблонов в перечислениях таким образом, чтобы компилятор уведомлял, когда определены новые члены перечисления (как это было бы при полном сопоставлении шаблонов по DU)?

Ответ №4:

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

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

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

Комментарии:

1. Я не согласен; в частности, новые значения enum могут быть добавлены между разными версиями зависимости, и код, который создает эти новые значения, не «сломан». Потому что . СЕТЕВЫЕ перечисления расширяемы по своей конструкции, принудительная обработка «неизвестных» значений действительно является особенностью.

2. Давайте сравним (А) текущий подход, использующий код в OP, и (Б) что произошло бы, если бы F # не принудительно обрабатывал неизвестные значения. И посмотрите, как они справляются с вашей проблемой добавления обновления зависимости в перечисление. В каждом случае, если вы запускаете код, игнорирующий предупреждения, при обнаружении неизвестного перечисления будет ошибка времени выполнения. Никакой разницы нет. Что, если обратить внимание на предупреждения? (A) не выдаст предупреждение и завершится сбоем во время выполнения. (B) выдаст предупреждение и заставит вас обновить соответствие шаблону. Итак, (B) лучше подходит для вашей ситуации.

3. Нет, вы неправильно поняли мой сценарий — я предполагаю, что вы скомпилируете свое приложение F # для версии v1 библиотеки с перечислением; затем вы обновляете свою зависимость до версии v2, которая добавляет новое значение к существующему типу перечисления без перекомпиляции (обычно это не считается критическим изменением). Затем, если F # не предупреждал, вы могли бы получить ошибку времени выполнения, когда компилятор указал, что в вашем коде не было ничего, что могло бы пойти не так.

4. Хорошо, но это кажется странным поведением для обновления зависимости без перекомпиляции. И с системой предупреждений вы также получите ошибку времени выполнения, поскольку вы каким-то образом отключили предупреждение. С помощью системы предупреждений вы получаете предупреждение о том, что что-то может пойти не так (минимально возможное) и вам придется вносить изменения, но ваши изменения обычно не предотвращают ошибки во время выполнения в таких ситуациях и повышают вероятность сбоя вашего кода в более распространенных ситуациях (перечисление изменено, вы перекомпилируете, но вы не обновляете поиск ошибки, потому что у вас есть обходной путь к предупреждению).

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