Как лучше всего объявить val как последовательность ранее объявленных val в блоке в Scala?

Довольно типичный вариант использования: объект (или класс) объявляет несколько открытых значений связанных типов, и он хотел бы объявить метод доступа, возвращающий коллекцию, содержащую все из них:

case class Ball(dia :Int)
object Balls {
    val tennis = Ball(7)
    val football = Ball(22)
    val basketball = Ball(24)
    val balls = Seq(tennis, football, basketball)
}

Этот пример явно нарушает DRY и подвержен ошибкам. Это может быть легко решено с помощью изменяемого состояния (например, добавление неявного Builder[Ball, Seq[Ball]] параметр к Ball конструктор. Однако это решение не без проблем. В частности, когда мы пытаемся обобщить решение и / или иметь иерархию классов, где каждый класс объявляет некоторые значения, момент, когда мы должны перейти от изменяемой частичной сводки к окончательному неизменяемому значению, неясен.

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

object Balls {
    import shapeless.{::, HNil}

    val (balls @
            tennis ::football::basketball::HNil
        ) =
            Ball(7)::Ball(22)::Ball(24)::HNil
}

Что довольно аккуратно, но становится неуправляемым, когда количество шаров или их инициализаторы не крошечные. Рабочей альтернативой было бы изменить все в HMap, но я обычно стараюсь избегать бесформенных зависимостей в публичном API. Кажется, что это может быть возможно выполнимо с продолжениями Scala, но я понятия не имею, как сделать объявления нелокальными для блока сброса.

РЕДАКТИРОВАТЬ: То, что я не подчеркнул раньше, и причина, почему scala.Enumeration не делает работу за меня, в том, что в реальном случае объекты не идентичны, а на самом деле являются составными структурами, конструкторы которых занимают несколько строк или более. Таким образом, хотя окончательный тип может быть одинаковым (или, по крайней мере, меня не интересуют детали), это не простое перечисление, и для удобства чтения важно, чтобы имя объявленного члена / ключа могло быть легко завязать визуально с его определением. Таким образом, мое бесформенное решение здесь, а также бесформенное предложенное основанное на Seq, очень восприимчивы к ошибкам "один за другим", когда вносится изменение в неправильное значение, ошибочно принимая его реальный идентификатор.

Конечно, реальный случай в настоящее время реализован аналогично scala.Enumeration, поддерживая последовательность значений, полученных методом унаследованного конструктора. Однако он страдает от всех проблем, которые делает Enumeration, и увеличивает вероятность ошибок при вызове конструктора вне фактического object Balls инициализатор или отбрасывание значения в условном блоке. Кроме того, мне было интересно, как это решается на чисто функциональных языках.

Есть идеи, как съесть пирог и съесть его?

2 ответа

Не уверен, что вы ищете, но что касается того, что вы пытались использовать Shapeless, я считаю, что вы можете достичь этого без него, и очень похоже на то, что вы только что сделали:

case class Ball(dia :Int)
object Balls {
    val balls@Seq( tennis, football, basketball ): Seq[Ball] = Ball(7)::Ball(22)::Ball(24)::Nil
}

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

РЕДАКТИРОВАТЬ

Рассматривали ли вы для моделирования данных в более наглядной форме. Посмотрим:

sealed trait Balls {
    def dia: Int
}
case object Football { val dia: Int = 22 }
case object Tennis { val dia: Int = 7 }
case object Basketball { val dia: Int = 24 }

object Balls {
    val values: Seq[Ball] = Football :: Tennis :: Basketball :: Nil
}

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

def kickBall( in: Ball ): Boolean = {
    in match {
        case f: Football => 
            true
        case b: Basketball =>
            // You shouldn't do this
            false
        case _ =>
            // Anything else
            false
    }
}

Использование sealed заставит вас определять все типы в одном и том же файле, а компилятор сообщит вам, когда вы забудете о случаях сопоставления с образцом.

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

Не функциональное решение, поэтому не правильный ответ на мой вопрос, но небольшая проблема "потери" значений, созданных методом конструктора, ответственного также за их сбор, может быть решена путем перемещения сборщика в unapply метод:

class Collector[T] {
    private[this] var seq :Seq[T]=Nil
    def items = seq
    def unapply(item :T) = synchronized { seq = item+:seq; Some(item) }
}

class Ball private (val dia :Int)

object Ball {
    val Ball = new Collector[Ball]
    implicit private def ball(dia :Int) = new Ball(dia)

    val Ball(basket) = 24
    val Ball(tennis) = 7
    val Ball(football) = 22
}

Хотя я предпочитаю это решение синтаксически, я не думаю, что выгода будет достаточно большой, чтобы компенсировать фактор путаницы по сравнению с простейшим заводским методом.

Другие вопросы по тегам