Декларативная проверка в Python

За последние два года я достаточно освоился с обоими PureScript и Haskell . Я узнал так много нового, погружаясь в экосистему чистого функционального программирования, и многие из этих методов можно применить к другим парадигмам. К сожалению, мир чистого FP может казаться немного другим измерением – где многие проблемы программирования имеют элегантные решения, но мир «обычного» программирования не знает об этих шаблонах.

Один из таких шаблонов называется «валидация в прикладном стиле», но я буду просто называть его «декларативной валидацией». В этом посте я объясню, как использовать эту технику, а затем создам небольшую библиотеку на Python реализация этих идей.

Многие из наших программ принимают ввод от пользователя. Часто нам необходимо проверить этот ввод перед продолжением обработки и, в случае ошибок, сообщить пользователю о любых проблемах. Существует несколько методов для выполнения такого рода проверки, но наиболее распространенным является написание некоторого императивного кода, который просматривает входные данные и составляет список ошибок. Если ошибок нет, предоставленные данные действительны, в противном случае – нет. Мы можем заключить результат, возвращаемый нашей проверкой, в объект, который указывает, была ли проверка успешной.

   @   класс данных   класс   Действительный  :  значение :  Любой  def  действует():  возвращаться  Правда  @   класс данных  класс Инвалид :  значение:   Любые   def  действует  ():  возвращаться Ложь  def   validate_name   ( название,  ошибки  ):  если нет  isinstance   ( название ,   str  )  или же  имя   ==   ""  :   ошибки  .   добавить   (  «имя должно быть непустой строкой»  )   def   validate_age   (возраст ,   ошибки  ):  если нет  isinstance   ( возраст,   int  ):   ошибки  .   добавить   (  "возраст должен быть целым"  )   элиф   возраст   <  11  :   ошибки   .   добавить   (  "возраст должен быть не менее 10 " )   def   проверить   ( данные):   ошибки   знак равно   validate_name   (данные .  получать ( "название"),   ошибки  )   validate_age   (данные.  получать (  "возраст"),  ошибки  )  если нет  ошибки  :  возвращаться Действительный(данные )  еще :  возвращаться Инвалид (  ошибок  )   

Хотя этот подход работает, все усложняется, когда у нас есть проверки, которые зависят от предыдущих результатов. Скажем, например, что мы хотим добавить новое, более сложное правило, гласящее, что если имя Дрю возраст должен быть не менее 0500 . Для этого необходимо указать имя и возраст, а также указать соответствующий тип. Но у нас нет удобного способа «повторно использовать» эту логику из существующего validate_name и validate_age функции. Один из подходов - просто перепроверить локально и предположить, что ошибки уже были добавлены, если типы неверны.

   def   validate_drew  (данные ,   ошибки  ):  если  ( нет  isinstance  (данные . получать  ( "название"),   str  )  или же нет  это экземпляр   ( данные.  получать("возраст" ),   int  )):  возвращаться  elif  данные.получать  ("название" )   ==  "Рисовал" а также данные.получать  ("возраст" )   <  40  :   ошибки  .   добавить   (  «Дрю, должно быть, старый»  )   

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

Недостатки подхода к проверке с отслеживанием состояния можно преодолеть, используя подход «синтаксического анализа». То есть мы декларативно описываем форму и тип ожидаемого ввода и возвращаем ошибку, если наши данные не соответствуют этим ожиданиям. Этот подход очень хорошо задокументирован в сообщении Анализируйте, не проверяйте . Синтаксический анализ - фантастическая альтернатива проверке с отслеживанием состояния, но этот стиль синтаксического анализа (часто называемый монадическим анализом) имеет один недостаток - он останавливает обработку, как только достигается первая ошибка. Мы хотели бы собрать как можно больше информации о недопустимом вводе для нашего пользователя.

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

Мы будем предоставлять две основные функции вместе с существующими Допустимые и Недействительные типы.

  1. validate_into позволяет нам вызвать предоставленную функцию со списком аргументов, предполагая, что все аргументы Допустимые . В противном случае он накапливает ошибки в любых Недействительных аргументы.
  2. а потом позволяет нам выполнить еще один «этап» проверки, предполагая, что объект функции Действителен . Если предметом функции является Недействительный , мы ничего не делаем.

Вы можете думать о validate_into как построение одного «этапа» нашего конвейера проверки и and_then как соединение двух этапов вместе. Ошибки любых проверок на этапе будут накапливаться, но если этап завершится неудачно, мы не будем запускать проверки для любых последующих этапов. Это означает, что мы должны разбивать наши проверки на этапы только тогда, когда данный этап зависит от допустимых значений из предыдущего этапа.

Давайте использовать эти две функции для повторной реализации наших проверок сверху. Сначала мы определим Person класс, в который мы будем помещать действительные данные.

   @   dataclass   класс  Человек :  название :   str  возраст:   int   

Теперь мы заново определим наши validate функция и ее помощники.

    def   validate_name   ( название):  если нет  isinstance   ( название,   str   ) или же название  ==   ""  :  возвращаться Инвалид (["name must be a non-empty string"])  еще:   return   Действительный   (название )   def   validate_age   ( возраст ):  если нет  isinstance   ( возраст,   int  ):  возвращаться Инвалид (["age must be an integer"])   elif   возраст   <  10  :  возвращаться Инвалид(["age must be at least 10"]) еще  :  возвращаться Действительный  ( возраст)   def   validate_drew   ( человек):  если человек . название  ==  "Рисовал" а также человек.возраст  <  40  :   return   Недействительный   (["Drew is old"])  еще :  возвращаться Действительный  (человек)   def   проверить   (данные ):  возвращаться  validate_into   ( Человек,   validate_name   (данные . получать  ( "название" )),   validate_age   ( данные.  получать("возраст" )),  ). а потом  (  validate_drew  )   

Здесь следует обратить внимание на несколько моментов. . Во-первых, каждая функция проверки стоит отдельно. Во-вторых, не происходит мутации входных данных. Каждая функция выполняет свои проверки и затем возвращает Действительный или Недействительное значение. Наконец, обратите внимание, что каждый Недействительный возвращает список ошибок. Это позволяет нашему накоплению происходить.

Давайте назовем наш проверьте функцию несколько раз и посмотрите, что произойдет:

   проверить   ({ "название":  Никто,   " возраст":  "Привет" ,  })   # => Недействительно (значение = [#             'name must be a non-empty string',#             'age must be an integer'])   проверить   ({ "название":  "Рисовал", "возраст" :   38  ,  })   # => Invalid (value = ['Drew is old'])   проверить   ({ "название":   «Джейн»  ,  "возраст" :   38  ,  })   # => Valid (value = Person (name = 'Джейн', возраст = 40))   

Обратите внимание, что второй этап наших проверок, а именно validate_drew , можно предположить, что весь его ввод Действует после первого этапа. Следовательно, нам не нужно ничего перепроверять относительно типов name или возраст перед выполнением нашей конкретной проверки (Дрю должен быть старым). Также обратите внимание, насколько легко было бы добавить новые проверки, если бы мы добавили новый аргумент в Person конструктор.

Мы можем представить, что библиотека для поддержки этого кода было бы довольно сложно. На практике это очень просто. Единственная функция, не входящая в стандартную библиотеку, которую мы используем, это curry из toolz , но если бы мы хотели отбросить эту зависимость, мы могли бы повторно реализовать

 curry  сами. 

  из  классы данных   импорт   класс данных  из  functools  Импортировать уменьшать из  toolz   import   карри   из  ввод  Импортировать Любой @  класс данных  класс Действительный :  значение:   Любые   def  действует  (себя ):  возвращаться Правда  def  применять  ( себя,  Другие):  если Другие . действует  ():  возвращаться Действительный ( себя.значение  (Другие .  значение))  еще:  возвращаться Другие  def  а потом ( себя,   f  ):  возвращаться  f   (себя.  значение)   @   класс данных  класс Инвалид :  значение :  Любой  def  действует(себя :  возвращаться Ложь  def  применять ( себя, Другие ):  если Другие . действует ():  возвращаться себя еще:  возвращаться Инвалид  (себя.  значение  +   Другие.значение )   def  а потом ( себя,   f  ):   return   self   def   validate_into   (  f  ,     args  ):  возвращаться уменьшать  (  лямбда   a  ,   b  :   a   . применять  (  b  ),   аргументы  ,  Действительный(карри  (  f  )))   

Приведенный выше код - это вся наша библиотека. Функция and_then относительно проста. . Если мы попытаемся связать новый этап проверок с Действительным , мы просто вызываем предоставленную функцию со значением внутри нашего Действительных . Если мы попытаемся связать новый этап проверок с Недействительным значение, мы просто игнорируем предоставленную функцию и возвращаем self .

validate_into кажется более сложной, поэтому давайте пошагово опишем, что она делает. Сначала мы карри предоставленную функцию . Это важно, потому что мы будем применять функцию по одному аргументу за раз, когда мы определяем, является ли каждый аргумент допустимым . Мы также помещаем эту каррированную функцию в Valid оболочка, которая начинается в Допустимый , прежде чем увидеть какие-либо аргументы. Затем, один за другим, мы применяем следующий аргумент к нашей «функции пока». В случае, если аргумент Действителен и «функция пока» действительна, мы просто вызываем функцию с аргументом и заново оборачиваем ее в

 Действительный .  Если «функция до сих пор»  Действительна , но новый аргумент  Недействительный , мы делаем новую «функцию пока»  Invalid  результат.  Наконец, что важно, если «функция до сих пор» уже  недействительна , и нам предоставлен новый  Недействительный , мы объединяем ошибки и заново оборачиваем результат в  Недействительный . 

Используя эти простые инструменты, мы можем написать сложные, глубоко вложенные валидаторы. Повторное использование наших валидаторов просто, поскольку они не более чем функции. Мы можем поместить их в пакет и поделиться обычно используемыми валидаторами (подумайте validate_presence ) в нашей кодовой базе.

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

Leave a comment

Your email address will not be published. Required fields are marked *