Приложение Monoid для подтипов не компилируется с оператором добавления, но работает при явном вызове

Я делаю моноид для комбинирования стратегий повторного выполнения, и RetryExecutor[T] является базовым типом. Я определил следующий базовый тип и моноид:

trait RetryExecutor[C] {
  def retry[T](f: C => T)(context: C): T

  def predicate: Option[Throwable]
  def application: Unit
  val retryEligible: PartialFunction[Throwable, Boolean]
}

object RetryExecutor {
  implicit def retryExecutorMonoid[A] = new Monoid[RetryExecutor[A]] {
  ...
}

и некоторые базовые типы, такие как:

case class LinearDelayingRetryExecutor[C](delayInMillis: Long)(val retryEligible: PartialFunction[Throwable, Boolean]) extends RetryExecutor[C] {
  override def predicate: Option[Throwable] = None
  override def application = Thread.sleep(delayInMillis)
}

case class RetryWithCountExecutor[C](maximumRetries: Int)(val retryEligible: PartialFunction[Throwable, Boolean])(implicit val logger: Logger) extends RetryExecutor[C] {
  var remainingTries = maximumRetries + 1

  override def application: Unit = {
    remainingTries = remainingTries - 1
  }

  override def predicate: Option[Throwable] = {
    if (remainingTries > 0) None
      else Some(RetryingException("Retry count of " + maximumRetries + " exceeded for operation"))
  }
}

И я могу объединить их вручную:

val valid: PartialFunction[Throwable, Boolean] = { case x: TestException => true }

val monoid = RetryExecutor.retryExecutorMonoid[Int]
val x = monoid.append(RetryWithCountExecutor[Int](3)(valid), LinearDelayingRetryExecutor(100)(valid))

но когда я пытаюсь использовать оператор добавления:

val x = RetryWithCountExecutor[Int](3)(valid) |+| LinearDelayingRetryExecutor(100)(valid)

Я получаю ошибку компиляции:

[error] /Users/1000306652a/work/src/test/scala/com/foo/bar/RetryExecutorSpec.scala:25: value |+| is not a member of com.foo.bar.retry.RetryWithCountExecutor[Int]
[error]   val k: RetryExecutor[Int] = RetryWithCountExecutor[Int](3)(valid) |+| BackingOffRetryExecutor[Int](100)(valid)

person PlexQ    schedule 03.10.2014    source источник


Ответы (1)


Вот та же проблема в гораздо более простом случае:

scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._

scala> Option(1) |+| Option(2)
res0: Option[Int] = Some(3)

scala> Monoid[Option[Int]].append(Some(1), Some(2))
res1: Option[Int] = Some(3)

scala> Some(1) |+| Some(2)
<console>:14: error: value |+| is not a member of Some[Int]
              Some(1) |+| Some(2)
                      ^

Проблема (на самом деле это не проблема, а скорее конструктивное решение) заключается в том, что Monoid не является ковариантным — наличие Monoid[Option[Int]] не означает, что у меня есть Monoid[Some[Int]].

Идеальное решение — предоставить конструкторы для подтипов, которые возвращают типы значений в качестве супертипа. Продолжая наш пример Option, Scalaz предоставляет эти конструкторы как some и none:

scala> some(1) |+| some(2)
res3: Option[Int] = Some(3)

scala> some(1) |+| none
res4: Option[Int] = Some(1)

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

person Travis Brown    schedule 03.10.2014
comment
Единственная проблема в том, что RetryExecutor[C] не Monoid. - person Mike Allen; 03.10.2014
comment
@MikeAllen: Верно, поскольку классы типов работают не так — весь смысл (немного преувеличивая) состоит в том, чтобы отделить такую ​​функциональность от наследования. RetryExecutor[A] имеет экземпляр Monoid, и Scalaz предоставляет некоторую неявную магию класса, которая поддерживает здесь синтаксис |+|. (Кстати, я не был противником вашего ответа, хотя в данном контексте это неправильно.) - person Travis Brown; 03.10.2014
comment
Но его код — это класс, не являющийся моноидом, который пытается выполнить |+| функцию, которой у него нет. Объявление implicit def retryExecutorMonoid[A] не определяет никаких аргументов, поэтому оно не может выполнить неявное преобразование экземпляра этого класса в моноид. Нет никакого волшебства, которое могло бы обеспечить операцию |+|, которую я вижу... - person Mike Allen; 03.10.2014
comment
Магия. - person Travis Brown; 04.10.2014
comment
А, хорошо - вижу. Потому что RetryWithCountExecutor — это подтип, а Monoid не является ковариантным. Извините, вы правы. - person Mike Allen; 04.10.2014
comment
Да, я не хочу звучать воинственно, но это действительно то, как Monoid работает в Scalaz. - person Travis Brown; 04.10.2014
comment
Было бы разумно определить сопутствующий объект для этих конкретных подтипов, который определил метод apply() для возврата RetryExecutor[A], а не подтип, как хороший способ просто добиться этого? - person PlexQ; 04.10.2014
comment
Это сработало бы, но это как бы противоречит ожиданиям людей относительно того, что должен делать apply для объектов-компаньонов (а именно, возвращать что-то статически типизированное в качестве класса-компаньона). Я бы сказал, что методы с соответствующими именами в RetryExecutor были бы лучше, но это в значительной степени вопрос вкуса. - person Travis Brown; 04.10.2014