Как управлять предполагаемым типом для связанных переменных при сопоставлении с образцом при наличии типов более высокого типа

(это основано на статье на http://bertails.org/2015/02/15/abstract-алгебраических-данных-типа)

Во-первых, я определяю абстрактную версию scala.Option.

import scala.language.higherKinds

trait OptionSig {
  type Option[+_]
  type Some[+A] <: Option[A]
  type None <: Option[Nothing]
}

abstract class OptionOps[Sig <: OptionSig] extends Extractors[Sig] {
  def some[A](x: A): Sig#Some[A]
  def none: Sig#None
  def fold[A, B](opt: Sig#Option[A])(ifNone: => B, ifSome: A => B): B
}

Я хочу иметь возможность использовать сопоставление с образцом для Sig#Option[A], чтобы Extractors выглядело так:

trait Extractors[Sig <: OptionSig] { self: OptionOps[Sig] =>

  object Some {
    def unapply[A](opt: Sig#Option[A]): scala.Option[A] =
      fold(opt)(scala.None, a => scala.Some(a))
  }

  object None {
    def unapply[A](opt: Sig#Option[A]): Option[Unit] =
      fold(opt)(scala.Some(()), _ => scala.None)
  }

}

Теперь я могу написать эту программу:

class Program[Sig <: OptionSig](implicit ops: OptionOps[Sig]) extends App {

  import ops._

  val opt: Sig#Option[Int] = some(42)

  opt match {
    case None(_)  => sys.error("")
    case Some(42) => println("yay")
    case Some(_)  => sys.error("")
  }

}

И я могу проверить это с этой реализацией.

trait ScalaOption extends OptionSig {

  type Option[+A] = scala.Option[A]
  type Some[+A]   = scala.Some[A]
  type None       = scala.None.type

}

object ScalaOption {

  implicit object ops extends OptionOps[ScalaOption] {

    def some[A](x: A): ScalaOption#Some[A] = scala.Some(x)

    val none: ScalaOption#None = scala.None

    def fold[A, B](opt: ScalaOption#Option[A])(ifNone: => B, ifSome: A => B): B =
      opt match {
        case scala.None    => ifNone
        case scala.Some(x) => ifSome(x)
      }

  }

}

object Main extends Program[ScalaOption]

Вроде работает, но есть один раздражающий момент, который я не могу понять.

С scala.Option тип s в Option(42) match { case s @ Some(42) => s } равен Some[Int]. Но с моим фрагментом выше это Sig#Option[Int], и вместо этого я хотел бы сделать его Sig#Some[Int].

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

trait Extractors[Sig <: OptionSig] { self: OptionOps[Sig] =>

  object Some {
    def unapply[A](s: Sig#Some[A]): scala.Option[A] =
      fold(s)(scala.None, a => scala.Some(a))
  }

  object None {
    def unapply(n: Sig#None): Option[Unit] =
      fold(n)(scala.Some(()), (_: Any) => scala.None)
  }

}

Но теперь я получаю предупреждения, подобные следующему:

[warn] Main.scala:78: abstract type pattern Sig#None is unchecked since it is eliminated by erasure
[warn]     case None(_)  => sys.error("")

Я не уверен, почему это происходит, поскольку Sig#None является подтипом Sig#Option[Int], и это известно во время компиляции.

Кроме того, время выполнения все еще в порядке, но предполагаемый тип все еще не тот, который я ожидал.

Итак, вопросы

  • почему здесь упоминается стирание типа, несмотря на информацию о подтипах?
  • как получить Sig#Option[Int] за s в (some(42): Sig#Option[Int]) match { case s @ Some(42) => s }



Ответы (1)


К сожалению, вы не можете делать то, что хотите. Проблема в том, что scalac не достаточно знает, что Sig#None <: Sig#Option[A], он должен быть в состоянии проверить, что значение, которое он передает в unapply, на самом деле является Sig#None. Это работает для scala.Option, потому что компилятор может сгенерировать проверку instanceof, чтобы убедиться, что тип действительно является типом Some или None, прежде чем передать его методу unapply. Если проверка не проходит, шаблон пропускаетсяunapply никогда не вызывается).

В вашем случае, поскольку scalac знает только, что opt является Sig#Option[Int], он не может ничего сделать, чтобы гарантировать, что значение на самом деле является Some или None, прежде чем передать его unapply.

Так что же он делает? Значение передается в любом случае! Что это значит? Что ж, давайте немного изменим ваши экстракторы:

trait Extractors[Sig <: OptionSig] { self: OptionOps[Sig] =>
  object Some {
    def unapply[A](s: Sig#Some[A]): scala.Option[A] =
      fold(s)(scala.None, a => scala.Some(a))
  }

  object None {
    def unapply(n: Sig#None): Option[Unit] =
      scala.Some(())
  }
}

Все, что мы сделали, это прекратили использование fold в случае None. Поскольку мы знаем, что аргумент должен быть Sig#None, зачем вообще вызывать fold, верно? Я имею в виду, мы не ожидаем, что здесь будет передано Sig#Some, верно?

Когда мы запустим этот пример, вы получите RuntimeException, потому что наше самое первое совпадение с шаблоном успешно и вызывает ???. В случае scala.Option шаблон потерпит неудачу, потому что он защищен сгенерированной проверкой instanceof.

Я привел еще один пример, показывающий еще одну опасность: мы добавляем небольшое ограничение к Sig#Some, что позволяет нам избежать fold и в случае Some: https://gist.github.com/tixxit/ab99b741d3f5d2668b91

В любом случае, ваш конкретный случай технически безопасен. Мы знаем, что вы использовали fold, поэтому использовать его с Sig#Option безопасно. Проблема в том, что scalac этого не знает.

person tixxit    schedule 29.04.2015
comment
Отличный ответ. Знаете ли вы простой способ увидеть, как компилируется сопоставление с образцом? Или какие-нибудь фрагменты документации/спецификации, объясняющие, как она компилируется? - person betehess; 30.04.2015
comment
@betehess Вы можете использовать :javap в REPL (не работает в 2.10). Вы можете увидеть пример сеанса здесь (см. строку 103 для instanceof): - person tixxit; 30.04.2015