Semantic surprise?

135 views
Skip to first unread message

Bill Venners

unread,
Jun 30, 2016, 2:54:45 PM6/30/16
to scala-internals
Hi All,

A question came up in a class today, and when I tried it out I was surprised that Cat compiled below. It seems like Cat below should not compile because the type of super in Furry is Artist, but the draw() method that gets invoked when Furry's super.draw() is invoked is the one from Cowboy. Although the draw() method in Cowboy has the same signature as the one in Artist, it has different semantics. I would think that the designer of Furry should be able to assume that since the type of super is Artist when Furry is compiled, that the super draw() method invoked by Furry's draw() method will always mean to draw a pretty picture and not something like pull a gun from a holster. Therefore I would think that since the compiler can't prove these semantics are the same, because Cowboy and Artist don't share any semantics with respect to draw(), that the compiler should reject this Cat. But it compiles.

trait Cowboy {
  /** Pull a gun from a holster and shoot */
  def draw(): Unit = println("Cowboy (bang!)")
}
class Artist {
  /** Create a picture with pencil and paper */
  def draw(): Unit = println("Artist")
}
trait Furry extends Artist {
  override def draw(): Unit = { println("Furry"); super.draw() }
}
trait HasLegs extends Artist {
  override def draw(): Unit = { println("HasLegs"); super.draw() }
}
trait FourLegged extends HasLegs {
  override def draw(): Unit = { println("FourLegged"); super.draw() }
}
class Cat extends Artist with Cowboy with Furry with FourLegged {
  override def draw(): Unit = { println("Cat"); super.draw() }
}

/*
scala> :load eat.scala
Loading eat.scala...
defined trait Cowboy
defined class Artist
defined trait Furry
defined trait HasLegs
defined trait FourLegged
defined class Cat

scala> (new Cat).draw()
Cat
FourLegged
HasLegs
Furry
Cowboy (bang!)
*/

Bill

Sébastien Doeraene

unread,
Jun 30, 2016, 4:04:00 PM6/30/16
to scala-internals
Hi Bill,

The behavior you observe seems perfectly normal to me. It's a normal consequence of linearization order. The linearized heritage of Artist is:
Artist > AnyRef

The direct superclass of Cat is Artist, so the tail of linearization of Cat is that of Artist, and the other traits come before:
Cat > FourLegged > HasLegs > Furry > Cowboy > Artist > AnyRef

Therefore, when Furry.draw() calls super.draw(), it resolves to Cowboy.draw().

Cheers,
Sébastien

--
You received this message because you are subscribed to the Google Groups "scala-internals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to scala-interna...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

martin odersky

unread,
Jun 30, 2016, 4:21:04 PM6/30/16
to scala-internals
No, super is virtual in a trait. That is, it depends how the trait is
mixed in. The details are in the definition of linearization.

- Martin

On Thu, Jun 30, 2016 at 8:54 PM, Bill Venners <bi...@artima.com> wrote:
> --
> You received this message because you are subscribed to the Google Groups
> "scala-internals" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to scala-interna...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.



--

Martin Odersky
EPFL and Lightbend

Rex Kerr

unread,
Jun 30, 2016, 4:44:52 PM6/30/16
to scala-i...@googlegroups.com
This is consistent with traits being as optimistic as plausible.  There are all sorts of somewhat-related cases, and every time as long as the types match and it _could_ work, the compiler happily assumes that it does.  Here are some others:

// Class can fill in for trait
class Salmon { def foo = "foo" }
trait Foo { def foo: String }
val fish = new Salmon with Foo {}

// Trait can fill in for trait
trait Bippy { def bip: String; def ppy: Int }
trait Biplane { def bip = "bip" }
trait Happy { def ppy = 7 }
val wish = new Bippy with Biplane with Happy {}

// Trait can fill in for class
abstract class Sparrow { def tweet: String }
trait Twitterer { def tweet: String = "@all #roflForLife" }
val gamish = new Sparrow with Twitterer {}

The override is taken to mean "it's all cool here, I found something that satisfies all the constraints", and at that point it's consistent with the optimistic principle.

class Bowl { def food = true }
trait Carrot { def food = true }
val a_ok = new Bowl with Carrot { override def food = false }

And if you don't override it tells you (if you do it this blatantly) how to make it work:

scala> val nogo = new Bowl with Carrot {}
<console>:12: error: <$anon: Bowl with Carrot>
inherits conflicting members:
  method food in class Bowl of type => Boolean  and
  method food in trait Carrot of type => Boolean
(Note: this can be resolved by declaring an override in
<$anon: Bowl with Carrot>.)

So, yes, I agree that the semantics might have changed, but the (optimistic) interpretation of override written explicitly at the point of unification is "I made sure it worked".  Maybe this is too optimistic.

--Rex




--

Adriaan Moors

unread,
Jun 30, 2016, 5:29:49 PM6/30/16
to scala-i...@googlegroups.com
Apropos 1, the name for this problem in the literature is "accidental override". I agree it's surprising that there can be a "super-call relationship" between unrelated types (Artist and Cowboy). In a sense this violates the intuition behind nominal subtyping (relation defined by explicit naming and relationships), and is more like structural subtyping (purely based on having members with the right name, no upfront declared parentage required)

Apropos 2, this extends clause avoids shots being fired: class Cat extends Cowboy with Artist with Furry with FourLegged

Bill Venners

unread,
Jul 1, 2016, 11:45:10 AM7/1/16
to scala-internals
Hi All,

To me this appears to be a soundness bug, either in Scala or my understanding. The reason is that it isn't type safe according to my understanding, which has always been that the type of super in Furry is Artist. It is a nominal type, not the structural type { def draw(): Unit }. The actual implementation of super.draw() to be invoked is indeed not known when we compile Furry, but the type of super is known.

Thus when invoking super.draw() in Furry, I should be able to rely not only that a draw(): Unit method will exist at runtime, but that its semantics matches that of the draw() method in Artist, which is "Create a picture with pencil and paper." Either this is a soundness problem or my understanding of the nominal nature of super type has been wrong and the type of super in Furry is actually structural { def draw(): Unit }.

Adrian's rewrite does not have the same soundness issue, because it hits Artist before it hits Cowboy and stops there, so that one should compile, but to me it seems like the one I originally posted should not compile.

Bill

som-snytt

unread,
Jul 16, 2016, 11:36:43 PM7/16/16
to scala-internals

The semantics would be less surprising if instead of Cowboy it was trait PussNBoots { def draw() = draw_sword() }

http://www.imdb.com/title/tt0448694/mediaviewer/rm407879168

The example does highlight that traits may make different demands of the programmer, the way only certain personalities would don a wing suit or scale Meru.

Am I comfortable allowing a client to determine the side-effects of "draw"? They might be drawing blood.
Reply all
Reply to author
Forward
0 new messages