Использование классов Scala Case в качестве карт де-факто

Это больше вопрос дизайна, чем что-либо еще...

Мне очень нравятся кейс-классы Scala, и я часто их использую. Однако я обнаружил, что часто включаю свои параметры в Options (или, скорее, Boxes в Lift) и устанавливаю значения по умолчанию, чтобы обеспечить гибкость и учесть, что пользователь может не всегда указывать все параметры. Я думаю, что я перенял эту практику от.

Мой вопрос в том, является ли это разумным подходом? Учитывая, что все может быть необязательным, может быть много шаблонов и проверок, вплоть до того, задаюсь ли я вопросом, не использую ли я просто свои классы case, такие как Map[String, Any], и задаюсь вопросом, не лучше ли мне просто использовать Map.

Приведу реальный пример. Здесь я моделирую денежный перевод:

case class Amount(amount: Double, currency: Box[Currency] = Empty)
trait TransactionSide
case class From(amount: Box[Amount] = Empty, currency: Box[Currency] = Empty, country: Box[Country] = Empty) extends TransactionSide
case class To(amount: Box[Amount] = Empty, currency: Box[Currency] = Empty, country: Box[Country] = Empty) extends TransactionSide
case class Transaction(from: From, to: To)

Относительно просто для понимания, я думаю. В самом простом случае мы можем объявить Transaction так:

val t = Transaction(From(amount=Full(Amount(100.0)), To(country=Full(US)))

Я уже могу представить, что вы думаете, что это многословно. А если указать все:

val t2 = Transaction(From(Full(Amount(100.0, Full(EUR))), Full(EUR), Full(Netherlands)), To(Full(Amount(150.0, Full(USD))), Full(USD), Full(US)))

С другой стороны, несмотря на то, что вам приходится везде разбрасывать Full, вы все равно можете сделать хорошее сопоставление с образцом:

t2 match {
  case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(country_from)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(country_to))) if country_from == country_to => Failure("You're trying to transfer to the same country!")
  case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(US)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(North_Korea))) => Failure("Transfers from the US to North Korea are not allowed!")
  case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(country_from)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(country_to))) => Full([something])
  case _ => Empty
}

Это разумный подход? Будет ли мне лучше обслуживаться с помощью Map? Или я должен использовать case-классы, но по-другому? Возможно, использовать целую иерархию классов дел для представления транзакций с различным количеством указанной информации?


person pr1001    schedule 01.07.2011    source источник


Ответы (2)


Использование класса case менее гибко, чем карта, поскольку вы можете назначать/получать доступ только к предварительно определенным полям. Вам нужно будет заранее построить полную иерархию классов кейсов.

С другой стороны, класс case предлагает своего рода «проверки времени компиляции», потому что все типы явно определены (в отличие от Map[String,Any]), и вы не можете по ошибке назначить/получить доступ к неуказанному полю. Классы case также должны быть быстрее, потому что вам не нужно просматривать хеш-таблицу карты, чтобы найти то, что вы ищете.

Проблема «многословия» возникает из-за неизменяемости классов case, но у вас будет точно такая же проблема с неизменяемыми картами. Решение похоже на линзы. Здесь очень хороший разговор:

http://www.youtube.com/watch?v=efv0SQNde5Q&list=PLEDE5BE0C69AF6CCE

person paradigmatic    schedule 01.07.2011
comment
Спасибо за ссылку на Lenses talk, сейчас слушаю. Но есть ли веб-страницы с учебником? Я хотел бы узнать больше! - person pr1001; 01.07.2011
comment
Никогда не находил их для scala, но должен существовать для haskell. - person paradigmatic; 01.07.2011

Если что-то действительно необязательно, то у вас действительно нет другого выбора. null не вариант (не каламбур).

Однако я бы настоятельно не советовал использовать тип окна Lift, если только он не нужен вам для работы с API Lift. Вы только вводите ненужную зависимость.

Я бы также серьезно подумал о том, действительно ли имеет смысл иметь Amount без указанной валюты. Если он действителен, то создание выделенного "нулевого объекта" для представления неуказанной валюты даст вам более чистый API:

class LocalCurrency extends Currency

В качестве альтернативы:

sealed trait Amount
case class LocalisedAmount(value: Double, currency: Currency) extends Amount
case class RawAmount(value: Double) extends Amount

Для подклассов TransactionSide мне кажется странным, что вы можете указать Currency отдельно от Amount (который уже включает понятие валюты). Я бы предпочел:

case class TxEnd(
    amount: Option[Amount] = None,
    country: Option[Country] = None)
case class Transaction(from: TxEnd, to: TxEnd)

Окончательно...

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

person Kevin Wright    schedule 01.07.2011
comment
Спасибо, ваши предложения звучат очень разумно. Причина, по которой у меня есть вторая валюта, заключается в том, что я могу анализировать такие строки, как $100 to DE или 100 USD to DE. LocalizedAmount и RawAmount могут помочь, но я не уверен. Прямо сейчас я сопоставляю строки с экстракторами: msg.split(" ").toList match { case Amount(amount_in) :: Currency(currency_in) :: "to" :: Country(country_out) :: Nil => ...}. Мне это очень нравится, и я не знаю, смогу ли я избежать второго Currency. - person pr1001; 01.07.2011
comment
Что касается Lift's Box, мне очень нравится Failure, и в любом случае этот код находится в контексте приложения Lift... - person pr1001; 01.07.2011
comment
В качестве альтернативы Box рассмотрим Either[Exception, Option[T]]. Преимущество состоит в том, что вы можете затем отобразить левую проекцию (например, myEither.left map {_.getMessage}), чтобы получить Either[String, Option[T]], подходящий для представления пользователю в качестве сообщения об ошибке. - person Kevin Wright; 01.07.2011
comment
Конечно, но я не вижу в этом никакого преимущества для себя перед Box. В любом случае, я не хочу углубляться в вечный спор о Box против Option/Either Scala, так что давайте пока оставим его там. ;-) - person pr1001; 01.07.2011