4
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

Why? If I am correct, the Liskov substitution principle is complied because all instances of Plugin are of a concrete type that is a mix that include a subtype of Base. Therefore, objects of type Basecan be replaced with objects of type Plugin without affecting the correctness of the program.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Readren
  • 994
  • 1
  • 10
  • 18

1 Answers1

5

Liskov principle says

https://en.wikipedia.org/wiki/Liskov_substitution_principle

Subtype Requirement: Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T.

Preconditions cannot be strengthened in the subtype.

Postconditions cannot be weakened in the subtype.

Invariants must be preserved in the subtype.

Liskov principle is about subtyping. Liskov principle is now not applicable because Plugin is not a subtype of Base

implicitly[Plugin <:< Base] // doesn't compile

(<:< is weaker than <: https://stackoverflow.com/a/75762663/5249621 but this is ok for us; if Plugin were a subtype of Base i.e. Plugin <: Base then moreover implicitly[Plugin <:< Base] would be).

I guess you're confusing either trait Plugin { base: Base => } with trait Plugin extends Base or subclasses (inheritance) with subtypes

What is the difference between a class and a type in Scala (and Java)?

What is the difference between Type and Class?

https://typelevel.org/blog/2017/02/13/more-types-than-classes.html

How to distinguish parameteric types?

What is a difference between refinement type and anonymous subclass in Scala 3?

trait Plugin extends Base means that Plugin is a subclass of Base. In particular, this means that Plugin is a subtype of Base, so all subtypes of Plugin are subtypes of Base. But trait Plugin { base: Base => } means that all subclasses of Plugin must be subclasses of Base. This doesn't mean that all subtypes of Plugin must be subtypes of Base (moreover, this doesn't mean that Plugin is a subtype of Base). Indeed, it's easy to declare a type

type SubPlugin <: Plugin

which is not a subtype of Base

implicitly[SubPlugin <:< Base] // doesn't compile

all instances of Plugin are of a concrete type that is a mix that include a subtype of Base.

Even if it's true that all values (OOP instances) of Plugin are values (instances) of Base this doesn't make Plugin a subtype of Base and this doesn't mean that all terms of type Plugin are terms of type Base. For example the term x in val x: Plugin = ??? is not a term of type Base.

You shouldn't think of a type as (only) a set of all values of the type (especially this becomes important in type-level programming where values are not so interesting at all). Sets can be a model (in logical sense: https://en.wikipedia.org/wiki/Model_theory) for types. But sets and types are different

https://cs.stackexchange.com/questions/91330/what-exactly-is-the-semantic-difference-between-set-and-type

https://math.stackexchange.com/questions/489369/difference-between-a-type-and-a-set

https://planetmath.org/11typetheoryversussettheory

For example if we define two abstract types

type A

type B

then there are no values of these types. So these sets would be equal in set-theoretic sense (an empty set). But these types are different. In homotopy type theory (HoTT, https://en.wikipedia.org/wiki/Homotopy_type_theory) 1 = 2 and 1 = 3 are two types without any values but two different types.

Therefore, objects of type Base can be replaced with objects of type Plugin without affecting the correctness of the program.

There are always programs that can't fail at runtime but that are rejected at compile time. For example if true then 1 else "a" can't fail at runtime but can be rejected by a language without subtyping e.g. Haskell (but not by Scala which thinks that this is Any). Scala rejects val x: Int = if (true) 1 else "a" although this can't fail at runtime either. Rich enough type system can't be sound and complete at the same time. Normally soundness is preferred over completeness

What is a sound programming language?

https://math.stackexchange.com/questions/105575/what-is-the-difference-between-completeness-and-soundness-in-first-order-logic

You may ask why wouldn't always trait Plugin { base: Base => } mean the same as trait Plugin extends Base. Well, this was design decision of Scala type-system creators. In particular, if this would be the same then two different traits trait A {this: B =>}, trait B {this: A =>} would have the same type.

Also in trait Plugin extends Base, Base must be a class/trait. But in trait Plugin { base: Base => }, Base can be a type.

Why scala self type is not a subtype of its requirement

What is the difference between self-types and trait subclasses?

Difference between trait inheritance and self type annotation

What is more Scala idiomatic: trait TraitA extends TraitB or trait TraitA { self: TraitB => }

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • While I agree that the difference between class VS type is very important. I find this a limitation of the compiler TBH. Unless I am missing an example where you can get a `Plugin` that is also not a `Base`, then the compiler should be able to genera the synthetic subtyping relationship. – Luis Miguel Mejía Suárez Apr 26 '23 at 12:40
  • @LuisMiguelMejíaSuárez What should the difference then be between `trait Plugin { base: Base => }` and `trait Plugin extends Base`? Why should then you have `trait Plugin { base: Base => }` if you already have `trait Plugin extends Base`? – Dmytro Mitin Apr 26 '23 at 12:57
  • 1
    It depends on which context you put that question. If all you care are subtyping semantics then yes you are right. But, from a modelling / design perspective the idea of `self` types _(IMHO)_ is to let the but force the user to decide which specific implementation to mix-in. Normal subtyping does not necessarily provides that, at least not as nicely. For example, I usually make the mix-in `sealed` because I don't want users to be able to add new implementations, just pick one of the few I provide them. – Luis Miguel Mejía Suárez Apr 26 '23 at 13:02
  • @LuisMiguelMejíaSuárez Ok, I see, with inheritance we specify a parent (`Base`) at the definition site of `Plugin`, with self-type we have to additionally specify a parent (`Base`) at the definition site of `Plugin` inheritor . Probably you have to make `Base` sealed in both cases. – Dmytro Mitin Apr 26 '23 at 15:07
  • So, look at this example: https://scastie.scala-lang.org/BalmungSan/O3WnacSERJWqPWQEmfnA8Q/2 as you can see `b4` will not compile because it doesn't mix-in a plugin. Thre is no way _(AFAIK)_ to get that same behaviour without self-types. – Luis Miguel Mejía Suárez Apr 26 '23 at 15:46
  • @LuisMiguelMejíaSuárez Doesn't `b4` compile either in inheritance case because `b4` doesn't mix-in a plugin? https://scastie.scala-lang.org/DmytroMitin/OlQ6L1aIRROZm5WRfd1p7Q/1 – Dmytro Mitin Apr 26 '23 at 16:09
  • BTW https://github.com/lampepfl/dotty/issues/7374 – Dmytro Mitin Apr 26 '23 at 16:38
  • 1
    Sure, but you can override it yourself: https://scastie.scala-lang.org/BalmungSan/B5Fs4D9aSwiKDtAygEuLuA And I don't want users to be able to do that, but pick one of my plugin implementations. _(for the record I have done this like 1 time in all my life, is not a common API design TBH)_ – Luis Miguel Mejía Suárez Apr 26 '23 at 16:46
  • @DmytroMitin As we can see in the `asBase` method, the compiler knows that all instances of `Plugin` are assignable to `Plugin & Base`. So, would it be a good idea to add to the language the possibility of obtaining the type that every self annotated class's instance will have outside of the definition? For example `Plugin.withSelfType` which would be of type `Plugin & Base`. In this way, the relationship between `Plugin` and `Base` would not have to be repeated in various places in the code. – Readren Apr 27 '23 at 16:25