trait Base
trait Plugin { base: Base =>
def asBase: Base & Plugin = this
}
class Mix extends Base, Plugin
val plug: Plugin = new Mix
val baseA: Base= plug.asBase
val baseB: Base = plug // snorts with "Found: Plugin. Required: Base
Почему?
Если я прав, принцип подстановки Лисков соблюдается, потому что все экземпляры Plugin
относятся к конкретному типу, который представляет собой смесь, включающую подтип Base
. Следовательно, объекты типа Base
можно заменить объектами типа Plugin
без ущерба для корректности программы.
Принцип Лисков гласит
https://en.wikipedia.org/wiki/Liskov_substitution_principle
Требование к подтипу: пусть
ϕ(x)
будет доказуемым свойством объектовx
типаT
. Тогдаϕ(y)
должно быть истинным для объектовy
типаS
, гдеS
является подтипомT
.Предусловия не могут быть усилены в подтипе.
Постусловия не могут быть ослаблены в подтипе.
Инварианты должны сохраняться в подтипе.
Принцип Лискова о подтипах. Принцип Лискова теперь неприменим, потому что Plugin
не является подтипом Base
implicitly[Plugin <:< Base] // doesn't compile
(<:<
слабее, чем <:
https://stackoverflow.com/a/75762663/5249621, но для нас это нормально; если бы Plugin
был подтипом Base
, т.е. Plugin <: Base
, то, кроме того, implicitly[Plugin <:< Base]
был бы).
Я думаю, вы путаете либо trait Plugin { base: Base => }
с trait Plugin extends Base
, либо подклассы (наследование) с подтипами.
В чем разница между классом и типом в Scala (и Java)?
В чем разница между Типом и Классом?
https://typelevel.org/blog/2017/02/13/more-types-than-classes.html
Как отличить параметрические типы?
В чем разница между типом уточнения и анонимным подклассом в Scala 3?
trait Plugin extends Base
означает, что Plugin
является подклассом Base
. В частности, это означает, что Plugin
является подтипом Base
, поэтому все подтипы Plugin
являются подтипами Base
.
Но trait Plugin { base: Base => }
означает, что все подклассы Plugin
должны быть подклассами Base
. Это не означает, что все подтипы Plugin
должны быть подтипами Base
(более того, это не означает, что Plugin
является подтипом Base
). Действительно, легко объявить тип
type SubPlugin <: Plugin
который не является подтипом Base
implicitly[SubPlugin <:< Base] // doesn't compile
все экземпляры
Plugin
относятся к конкретному типу, который представляет собой смесь, включающую подтипBase
.
Даже если верно, что все значения (экземпляры ООП) Plugin
являются значениями (экземплярами) Base
, это не делает Plugin
подтипом Base
, и это не означает, что все термины типа Plugin
являются терминами типа Base
. Например, термин x
в val x: Plugin = ???
не является термином типа Base
.
Вы не должны думать о типе как (только) о наборе всех значений типа (особенно это становится важным в программировании на уровне типов, где значения вообще не так интересны). Наборы могут быть моделью (в логическом смысле: https://en.wikipedia.org/wiki/Model_theory) для типов. Но наборы и типы разные
https://math.stackexchange.com/questions/489369/difference-between-a-type-and-a-set
https://planetmath.org/11typetheoryversussettheory
Например, если мы определяем два абстрактных типа
type A
type B
то нет значений этих типов. Таким образом, эти множества были бы равны в теоретико-множественном смысле (пустое множество). Но эти типы разные. В теории гомотопических типов (HoTT, https://en.wikipedia.org/wiki/Homotopy_type_theory) 1 = 2
и 1 = 3
— это два типа без каких-либо значений, но два разных типа.
Следовательно, объекты типа
Base
можно заменить объектами типаPlugin
без ущерба для корректности программы.
Всегда есть программы, которые не могут дать сбой во время выполнения, но отклоняются во время компиляции. Например, if true then 1 else "a"
не может дать сбой во время выполнения, но может быть отклонен языком без подтипа, например. Haskell (но не Scala, который думает, что это Any
). Scala отклоняет val x: Int = if (true) 1 else "a"
, хотя это также не может привести к сбою во время выполнения. Достаточно богатая система типов не может быть полноценной и полной одновременно. Обычно надежность предпочтительнее полноты
Что такое звуковой язык программирования?
Вы можете спросить, почему trait Plugin { base: Base => }
не всегда означает то же, что и trait Plugin extends Base
. Что ж, это дизайнерское решение создателей системы типов Scala. В частности, если бы это было одно и то же, то два разных признака trait A {this: B =>}
, trait B {this: A =>}
имели бы один и тот же тип.
Также в trait Plugin extends Base
Base
должен быть классом/чертой. Но в trait Plugin { base: Base => }
Base
может быть типом.
Почему тип scala self не является подтипом его требования
В чем разница между я-типами и подклассами черт?
Разница между наследованием признаков и аннотацией собственного типа
Что более идиоматично в Scala: trait TraitA extends TraitB или trait TraitA { self: TraitB => }
Хотя я согласен, что разница между классом VS типа очень важна. Я считаю это ограничением компилятора TBH. Если я не упустил пример, в котором вы можете получить Plugin
, который также не является Base
, тогда компилятор должен иметь возможность генерировать отношения синтетического подтипа.
@LuisMiguelMejíaSuárez Какая тогда должна быть разница между trait Plugin { base: Base => }
и trait Plugin extends Base
? Зачем тогда вам иметь trait Plugin { base: Base => }
, если у вас уже есть trait Plugin extends Base
?
Это зависит от того, в каком контексте вы ставите этот вопрос. Если все, что вас волнует, это семантика подтипов, то да, вы правы. Но с точки зрения моделирования/дизайна идея self
типов (ИМХО) состоит в том, чтобы позволить пользователю решить, какую конкретную реализацию смешивать, но заставить его решить. Нормальное подтипирование не обязательно обеспечивает это, по крайней мере, не так хорошо. Например, я обычно делаю микс-ин sealed
, потому что я не хочу, чтобы пользователи могли добавлять новые реализации, просто выберите одну из немногих, которые я им предоставлю.
@LuisMiguelMejíaSuárez Хорошо, я вижу, при наследовании мы указываем родителя (Base
) в месте определения Plugin
, при самотипировании мы должны дополнительно указать родителя (Base
) в месте определения Plugin
наследника. Вероятно, вам придется сделать Base
запечатанным в обоих случаях.
Итак, посмотрите на этот пример: scastie.scala-lang.org/BalmungSan/O3WnacSERJWqPWQEmfnA8Q/2 как видите b4
не будет компилироваться, потому что не смешивается с плагином. Нет никакого способа (насколько мне известно) получить такое же поведение без самотипов.
@LuisMiguelMejíaSuárez Не b4
компилируется в случае наследования, потому что b4
не смешивает плагин? scastie.scala-lang.org/DmytroMitin/OlQ6L1aIRROZm5WRfd1p7Q/1
Кстати github.com/lampepfl/dotty/issues/7374
Конечно, но вы можете переопределить его самостоятельно: scastie.scala-lang.org/BalmungSan/B5Fs4D9aSwiKDtAygEuLuA И я не хочу, чтобы пользователи могли это делать, но выберите одну из моих реализаций плагина. (для справки, я делал это как 1 раз за всю свою жизнь, это не обычный дизайн API, TBH)
@DmytroMitin Как видно из метода asBase
, компилятор знает, что все экземпляры Plugin
можно присвоить Plugin & Base
. Итак, было бы хорошей идеей добавить в язык возможность получения типа, который будет иметь каждый экземпляр самоаннотированного класса вне определения? Например, Plugin.withSelfType
, который будет иметь тип Plugin & Base
. Таким образом, отношения между Plugin
и Base
не должны повторяться в разных местах кода.
Scastie воспроизводит проблему: scastie.scala-lang.org/BalmungSan/D0ZAd9EIQ1KIUxn52IGl3Q/2