Почему универсальные типы не работают с наследованием в Scala?

Итак, вот код:

package week4
object expr {
  abstract class Expr[T] {
    def eval:T = this match {
      case Number(x)   => x
      case Sum(e1, e2) => e1.eval + e2.eval
    }
    def show: String = this match {
      case Number(x)   => "" + x
      case Sum(e1, e2) => "(" + e1.show + "+" + e2.show + ")"
    }
  }
  case class Number[T](val value: T) extends Expr {
  }
  case class Sum[T](val e1: Expr[T], val e2: Expr[T]) extends Expr {
  }
}

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

конструктор не может быть создан для ожидаемого типа; найдено: week4.expr.Number[T(в классе Number)] требуется: week4.expr.Expr[T(в классе Expr)] Примечание: ничего ‹: T (и week4.expr.Number[T] ‹: week4. expr.Expr[Nothing]), но класс Expr инвариантен в типе T. Вместо этого вы можете определить T как +T.

Что я делаю не так?


person Himanshu Mishra    schedule 22.07.2016    source источник
comment
Если предоставленный ответ действителен, было бы неплохо, если бы вы могли пометить его как принятый. :-)   -  person stefanobaghino    schedule 06.01.2018
comment
Взял мое сладкое время.   -  person Himanshu Mishra    schedule 28.10.2019
comment
Лучше поздно, чем никогда. :) Спасибо!   -  person stefanobaghino    schedule 29.10.2019


Ответы (1)


В вашем коде в основном две ошибки:

  • При расширении Expr вы забыли передать параметр типа
  • В ветке Sum вашего сопоставления с образцом вы пытаетесь суммировать два T, не предоставляя компилятору достаточных доказательств того, что оператор + определен для типа T.

Вот пересмотренное решение, которое работает:

object expr {

  abstract class Expr[T](implicit evidence: Numeric[T]) {
    def eval: T = this match {
      case Number(x)   => x
      case Sum(e1, e2) => evidence.plus(e1.eval, e2.eval)
    }
    def show: String = this match {
      case Number(x)   => "" + x
      case Sum(e1, e2) => "(" + e1.show + "+" + e2.show + ")"
    }
  }

  case class Number[T : Numeric](val value: T) extends Expr[T]

  case class Sum[T : Numeric](val e1: Expr[T], val e2: Expr[T]) extends Expr[T]

}

Вот пример запуска в оболочке Scala:

scala> import expr._
import expr._

scala> Sum(Sum(Number(2), Number(3)), Number(4))
expression: expr.Sum[Int] = Sum(Sum(Number(2),Number(3)),Number(4))

scala> println(expression.show + " = " + expression.eval)
((2+3)+4) = 9

Я уверен, что отсутствующий параметр типа для конструктора типа Expr просто отвлекает внимание и не требует дополнительных объяснений.

Код в моем примере вводит неявный параметр evidence и использование класса типов Numeric. В Scala класс типов — это шаблон, который использует черты и неявные параметры для определения возможностей классов с определенной степенью гибкости.

Чтобы суммировать два общих значения, компилятор должен иметь способ узнать, что два T знают, как суммироваться. Однако числовые типы не входят в иерархию типов, которую можно использовать, говоря, что T является подтипом гипотетического класса Number (написав что-то вроде abstract class Expr[T <: Number]).

Однако стандартная библиотека Scala представила класс типов Numeric, который, по сути, представляет собой трейт, определяющий набор операций, имеющих смысл для всех числовых типов (отсюда и название). Метод plus остается нереализованным для тех, кто хочет придерживаться этой черты.

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

Вот пример:

scala> val expression = Sum(Sum(Number(2.0), Number(3.0)), Number(4.0))
expression: expr.Sum[Double] = Sum(Sum(Number(2.0),Number(3.0)),Number(4.0))

scala> println(expression.show + " = " + expression.eval)
((2.0+3.0)+4.0) = 9.0

Указание неявного доказательства, подобного этому, может быть сделано явно (как в abstract class Expr[T](implicit evidence: Numeric[T])) или с использованием так называемой нотации «связанной с контекстом» (как в case class Number[T : Numeric]), которая в основном является синтаксическим сахаром для явного варианта, который отказывается явно ссылаться на экземпляр класса типа. . Я использовал явный вариант в первом случае, потому что мне нужно было сослаться на экземпляр класса типа в моем коде, чтобы на самом деле суммировать два значения (evidence.plus(e1.eval, e2.eval)), но я использовал нотацию «связанной с контекстом» в последнем случае, поскольку я считаю это более естественным и удобочитаемый.

При желании вы также можете реализовать Numeric[T] для своих собственных классов (например, Numeric[Rational]) без необходимости иметь дело со статической иерархией типов.

Это, конечно, очень поспешное объяснение классов типов: для более подробного объяснения я предлагаю вам это очень хорошее запись в блоге по теме.

person stefanobaghino    schedule 22.07.2016
comment
Также предложил бы прочитать о дисперсии типов в дополнение к классам типов. - person sebszyller; 22.07.2016
comment
@3jckd абсолютно, это может стать довольно важной темой при работе с иерархиями типов. Если вы случайно прочитали, что такое дисперсия типов, и в конечном итоге не поняли этого, это изображение — мой личный фаворит, который может вам помочь: twitter.com/stefanobaghino/status/597016186820423680 - person stefanobaghino; 22.07.2016
comment
Ключевое слово new можно опустить при создании экземпляров классов case, таких как Sum и Number, или я что-то упустил? - person isaias-b; 22.07.2016
comment
@isaias-b Вы совершенно правы, поскольку компилятор создает сопутствующие объекты для классов case с методом apply в качестве ярлыка для конструктора. Спасибо за совет, я немедленно отредактирую ответ. - person stefanobaghino; 22.07.2016