Класс случая и линеаризация признаков

Предположим, что я хочу написать класс case Stepper следующим образом:

case class Stepper(step: Int) {def apply(x: Int) = x + step}

Он поставляется с красивой реализацией toString:

scala> Stepper(42).toString
res0: String = Stepper(42)

но это не действительно функция:

scala> Some(2) map Stepper(2)
<console>:10: error: type mismatch;
 found   : Stepper
 required: Int => ?
              Some(2) map Stepper(2)

Обходной путь заключается в реализации признака Function...

case class Stepper(step: Int) extends (Int => Int) {def apply(x: Int) = x + step}

Но тогда я не могу получить бесплатную реализацию toString:

scala> Stepper(42).toString
res2: java.lang.String = <function1>

Тогда возникает вопрос: могу ли я иметь лучшее из этих двух миров? Есть ли решение, где у меня есть хорошая реализация toString для бесплатного И реализация признака Function. Другими словами, существует ли способ применения линеаризации таким образом, чтобы наконец был применен синтаксический сахар case class?

Ответ 1

Вопрос не связан с линеаризацией. В case-classes toString - это метод, автоматически генерируемый компилятором тогда и только тогда, когда Any.toString не переопределяется в конечном типе.

Однако ответ частично связан с линеаризацией - нам нужно переопределить Function1.toString с помощью метода, который был бы сгенерирован компилятором, если бы не версия, введенная Function1:

trait ProperName extends Product {
  override lazy val toString = scala.runtime.ScalaRunTime._toString(this)
}

// now just mix in ProperName and... magic!
case class Stepper(step: Int) extends (Int => Int) with ProperName {
  def apply(x:Int) = x+step
}

Тогда

println(Some(2) map Stepper(2))
println(Stepper(2))

Произведет

Some(4)
Stepper(2)

Обновление

Вот версия признака ProperName, которая не полагается на недокументированный API-метод:

trait ProperName extends Product {
  override lazy val toString  = {
    val caseFields = {
       val arity = productArity
       def fields(from: Int): List[Any] =
         if (from == arity) List()
         else productElement(from) :: fields(from + 1)
       fields(0) 
    }
    caseFields.mkString(productPrefix + "(", ",", ")")
  }
}

Альтернативная реализация toString получена из исходного кода для исходного метода _toString scala.runtime.ScalaRunTime._toString.

Обратите внимание, что эта альтернативная реализация по-прежнему основана на предположении, что класс case всегда расширяет черту Product. Несмотря на то, что последнее верно, как из Scala 2.9.0, и является фактом, который известен и полагается некоторыми членами сообщества Scala, он официально не зарегистрирован как часть Scala Спецификация языка.

Ответ 2

РЕДАКТИРОВАТЬ: Как насчет переопределения toString?

case class Stepper(step: Int) extends (Int => Int) {
  def apply(x: Int) = x + step
  override def toString = "Stepper(" + step + ")"
}

Ответ 3

Вы можете использовать неявное преобразование, чтобы Stepper рассматривалось как функция только при необходимости:

case class Stepper(step: Int) { def apply(x: Int) = x + step }

implicit def s2f(s: Stepper) = new Function[Int, Int] {
  def apply(x: Int) = s.apply(x)
}

Теперь вы получаете класс case toString при вызове Stepper(42).toString, но Some(2) map Stepper(2) также работает по желанию.

(Обратите внимание, что я был более подробным, чем необходимо выше, чтобы очистить механику. Вы также можете написать implicit def s2f(s: Stepper) = s.apply _ или любое количество других более сжатых формулировок).