Цепочка проверки в Scala

У меня есть Scala case class, содержащий информацию о конфигурации командной строки:

case class Config(emailAddress: Option[String],
                  firstName: Option[String]
                  lastName: Option[String]
                  password: Option[String])

Я пишу функцию проверки, которая проверяет, что каждое из значений является Some:

def validateConfig(config: Config): Try[Config] = {
  if (config.emailAddress.isEmpty) {
    Failure(new IllegalArgumentException("Email Address")
  } else if (config.firstName.isEmpty) {
    Failure(new IllegalArgumentException("First Name")
  } else if (config.lastName.isEmpty) {
    Failure(new IllegalArgumentException("Last Name")
  } else if (config.password.isEmpty) {
    Failure(new IllegalArgumentException("Password")
  } else {
    Success(config)
  }
}

но если я понимаю монады из Haskell, кажется, что я должен иметь возможность связать проверки вместе (псевдосинтаксис):

def validateConfig(config: Config): Try[Config] = {
  config.emailAddress.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("Email Address")) >>
  config.firstName.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("First Name")) >>
  config.lastName.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("Last Name")) >>
  config.password.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("Password"))
}

Если какое-либо из выражений config.XXX возвращает Failure, все это (validateConfig) должно завершиться ошибкой, иначе должно быть возвращено Success(config).

Есть ли способ сделать это с помощью Try или, может быть, какого-то другого класса?


person Ralph    schedule 12.09.2013    source источник
comment
Мой ответ здесь содержит несколько примеров и обсуждение (но с использованием Scalaz).   -  person Travis Brown    schedule 12.09.2013
comment
Можно глупый вопрос? Если все эти вещи необходимы в соответствии с вашими проверками, то почему они должны быть необязательными в классе case?   -  person cmbaxter    schedule 13.09.2013
comment
@cmbaxter: точно моя мысль...   -  person Erik Kaplun    schedule 13.09.2013
comment
Я использую scopt (github.com/scopt/scopt) для анализа командной строки. Класс case Config является неизменяемым и изменяется путем копирования. Он начинается со значений по умолчанию для каждого поля, и они изменяются по мере анализа параметров. Мне нужно каким-то образом по умолчанию указать, что параметр не установлен; Option[String] кажется намного лучше, чем null. Я не использую функциональность required в scopt, потому что хочу упростить тестирование отдельных параметров командной строки.   -  person Ralph    schedule 13.09.2013
comment
Это действительно звучит так, как если бы размещение параметров в коллекции (возможно, на карте) вместе с их ограничениями (скорее всего, как замыкания) и манипулирование ими с помощью for comprehensions было бы лучшим совпадением. Вы можете найти много объяснений в Интернете о том, что for comprehensions монадичны. Вот один из них: debasishg.blogspot.co.uk /2008/03/   -  person itsbruce    schedule 16.09.2013
comment
Но вам может понравиться это лучше: 10857973/scala-return-has-its-place Я думаю, что это демонстрирует то, что вы хотите сделать, только на более подходящем примере.   -  person itsbruce    schedule 17.09.2013


Ответы (3)


Довольно просто преобразовать каждый Option в экземпляр правильной проекции Either:

def validateConfig(config: Config): Either[String, Config] = for {
  _ <- config.emailAddress.toRight("Email Address").right
  _ <- config.firstName.toRight("First Name").right
  _ <- config.lastName.toRight("Last Name").right
  _ <- config.password.toRight("Password").right
} yield config

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

Если вы предпочитаете получить Try, вы можете просто преобразовать полученное Either:

import scala.util._

val validate: Config => Try[Config] = (validateConfig _) andThen (
  _.fold(msg => Failure(new IllegalArgumentException(msg)), Success(_))
)

Я бы хотел, чтобы стандартная библиотека предоставила более удобный способ сделать это преобразование, но это не так.

person Travis Brown    schedule 12.09.2013
comment
Действительно ли люди используют такой код в реальной жизни? Если да, то должно ли это быть читабельным и/или ремонтопригодным? (Это искренний вопрос — я вполне мог упустить что-то важное здесь) - person Erik Kaplun; 13.09.2013
comment
@ErikAllik: я постоянно использую такие вещи, как мой validateConfig, и лично нахожу это намного понятнее, чем, например. большие блоки условных предложений в вопросе. Преобразование в Try некрасиво, но его можно было бы сделать более универсальным — это то, что должно поддерживаться стандартной библиотекой, и даже если это не так, вам нужно определить ключевые части только один раз. - person Travis Brown; 13.09.2013
comment
@ErikAllik Вернулся к этому вопросу и увидел это, что является наиболее подходящим ответом с точки зрения того, чего пытается достичь ОП. По сути, это самая минимальная монадическая реализация цепочки ответственности. Я понятия не имею, почему Ральф считает, что его версия лучше. Судя по этому свидетельству, он действительно не вникает в монады. - person itsbruce; 02.10.2013

Это case-класс, так почему бы вам не использовать сопоставление с образцом?

def validateConfig(config: Config): Try[Config] = config match {
  case Config(None, _, _, _) => Failure(new IllegalArgumentException("Email Address")
  case Config(_, None, _, _) => Failure(new IllegalArgumentException("First Name")
  case Config(_, _, None, _) => Failure(new IllegalArgumentException("Last Name")
  case Config(_, _, _, None) => Failure(new IllegalArgumentException("Password")
  case _ => Success(config)
}

В вашем простом примере моим приоритетом было бы забыть монады и цепочки, просто избавиться от этого неприятного if...else запаха.

Однако в то время как класс case отлично работает для короткого списка, для большого количества параметров конфигурации это становится утомительным и возрастает риск ошибки. В этом случае я бы рассмотрел что-то вроде этого:

  1. Добавьте метод, который возвращает карту ключей-> значений параметров конфигурации, используя имена параметров в качестве ключей.
  2. Попросите метод Validate проверить, является ли какое-либо значение на карте None
  3. Если такого значения нет, вернуть успех.
  4. Если хотя бы одно значение совпадает, верните это имя значения с ошибкой.

Итак, если предположить, что где-то определено

type OptionMap = scala.collection.immutable.Map[String, Option[Any]]

и класс Config имеет такой метод:

def optionMap: OptionMap = ...

то я бы написал Config.validate так:

def validate: Either[List[String], OptionMap] = {
  val badOptions = optionMap collect { case (s, None) => s }
  if (badOptions.size > 0)
    Left(badOptions)
  else
    Right(optionMap)
}

Итак, теперь Config.validate возвращает либо Left, содержащее имя всех неверных параметров, либо Right, содержащее полную карту параметров и их значения. Честно говоря, вероятно, не имеет значения, что вы поместите в Right.

Теперь все, что хочет проверить Config, просто вызывает Config.validate и проверяет результат. Если это Left, он может выдать IllegalArgumentException, содержащий одно или несколько названий неверных вариантов. Если это Right, он может делать все, что захочет, зная, что Config действителен.

Таким образом, мы могли бы переписать вашу функцию validateConfig как

def validateConfig(config: Config): Try[Config] = config.validate match {
  case Left(l) => Failure(new IllegalArgumentException(l.toString))
  case _ => Success(config)
}

Вы видите, насколько более функциональным становится и объектно-ориентированный подход?

  • Нет императивной цепочки if...else
  • Объект Config проверяет себя
  • Последствия недопустимости объекта Config остаются на усмотрение более крупной программы.

Я думаю, что реальный пример был бы еще более сложным. Вы проверяете параметры, говоря: «Содержит ли он Option[String] или None?» но не проверяя действительность самой строки. Действительно, я думаю, что ваш класс Config должен содержать карту опций, где имя сопоставляется со значением и с анонимной функцией, которая проверяет строку. Я мог бы описать, как расширить приведенную выше логику для работы с этой моделью, но я думаю, что оставлю это вам в качестве упражнения. Я дам вам подсказку: вы можете захотеть вернуть не только список неудачных вариантов, но и причину отказа в каждом случае.

Да, кстати... Я надеюсь, что ни один из вышеперечисленных не означает, что я думаю, что вы должны хранить параметры и их значения как optionMap внутри объекта. Я думаю, что полезно иметь возможность извлекать их таким образом, но я бы никогда не поощрял такое раскрытие фактического внутреннего представления;)

person itsbruce    schedule 12.09.2013

Вот решение, которое я придумал после некоторого поиска и чтения scaladocs:

def validateConfig(config: Config): Try[Config] = {
  for {
    _ <- Try(config.emailAddress.
             getOrElse(throw new IllegalArgumentException("Email address missing")))
    _ <- Try(config.firstName.
             getOrElse(throw new IllegalArgumentException("First name missing")))
    _ <- Try(config.lastName.
             getOrElse(throw new IllegalArgumentException("Last name missing")))
    _ <- Try(config.password.
             getOrElse(throw new IllegalArgumentException("Password missing")))
  } yield config
}

Подобно ответу Трэвиса Брауна.

person Ralph    schedule 12.09.2013
comment
Почему вы хотите связать их вот так? Это не решение, которое вообще хорошо соответствует данной проблеме. Вы хотите поэкспериментировать с техникой? - person itsbruce; 13.09.2013
comment
Вы хотите поэкспериментировать с техникой? -- частично. Я также пытаюсь приспособить другие проверки (например: находится ли TCP-порт в диапазоне?). Монадический подход легко позволит это сделать, не делая особого случая. - person Ralph; 13.09.2013
comment
Но запись каждой проверки в последовательности делает все частным случаем, а сам класс почти невозможно чисто расширить. Ничто об этом не является хорошим стилем FP. Вы можете выполнять произвольные проверки, просто сохраняя замыкание с каждым параметром конфигурации. - person itsbruce; 16.09.2013