3

Why is f-bounded polymorphism in Scala commonly implemented with an upper type bound as well as a self type like

trait MyTrait[A <: MyTrait[A]] { self: A =>
  …
}

and not simply with just a self type like

trait MyTrait[A] { self: A =>
  …
}

The upper type bound in the first example seems to be unnecessary. At least I can't find any benefits of using one. Am I overlooking something here? The type bounds does however stand in the way of usages like

def func[A](implicit ev: A <:< MyTrait[A]) = ???

(of course in this simplistic and contrived example a func[A <: MyTrait[A]] would resolve the issue but that might not be possible in a more complex setting)

I found f-bounded polymorphism implemented with a type bound in multiple libraries and even in an introductory blog post about the topic (https://tpolecat.github.io/2015/04/29/f-bounds.html) and was wondering if it would be better to omit the type bound in general.

mrArkwright
  • 254
  • 2
  • 10

1 Answers1

3

So, the idea of trait MyTrait[A <: MyTrait[A]] { self: A => ... } is to force A to be the type of the current implementation.

If you omit the upper bound, then, aside from Foo not being able to do very much with its type parameter (it knows nothing about its members), you can also do things like this, which is not very useful.

   trait Foo[A] { self: A => }
   trait Bar 
   class Baz extends Foo[Bar] with Bar

Or even just class Bat extends Foo[Any]

Dima
  • 39,570
  • 6
  • 44
  • 70
  • I guess that `trait Bar with Foo` is supposed to be `trait Bar extends Foo[Bar]`? That whole construction (including your definition of `Baz`) is also possible _with_ the upper bound and is a known limitation of the expressiveness of f-bounded polymorphism in Scala. The example with the `Any` seems indeed to be a notable difference. I also don't unterstand what you mean by "Foo not being able to do very much with its type parameter". Do you have an example what `Foo` would be able to do with its type parameter with the upper bound that it can't do without it? – mrArkwright May 24 '21 at 12:59
  • @mrArkwright no with the upper bound the parameter to `Foo` would have to be a subclass of `Foo`, which `Bar` is not, so `Foo[Bar]` would be impossible. – Dima May 24 '21 at 13:11
  • @mrArkwright Re. "what Foo can do with upper bound" ... well, I dunno ... `def foo: A = this` ? – Dima May 24 '21 at 13:13
  • I'm sorry, but you are mistaken. I just compiled those three lines of code _with_ the upper bound (and with `trait Bar extends Foo[Bar]`. Can you please confirm, that this is what you meant? Or ideally edit your answer accordingly.) Please go ahead any try for yourself. – mrArkwright May 24 '21 at 13:34
  • `def foo: A = this` is also possible without the upper bound. So that's no difference there. – mrArkwright May 24 '21 at 13:34
  • @mrArkwright Sorry, I wasn't clear. How about `def copy: A`? In general, I'd say if you are having trouble coming up with a use case, you probably don't need f-bounded polymorphism in the first place :) – Dima May 24 '21 at 13:41
  • What about `def copy: A` wouldn't be possible without the upper bound? I don't have trouble coming up with a use case. `def foo: A = this` is a perfectly fine example of what can be achieved by it. But that wasn't my question. – mrArkwright May 24 '21 at 13:45
  • @mrArkwright I edited the answer, sorry (what I had there before didn't actually compile)... The whole idea was that `Bar` does _not_ extend `Foo`. Without upper bound for the parameter, you can create instances of `Foo` (like `Baz`) who `copy` method ends up returning things that are not `Foo` – Dima May 24 '21 at 13:49
  • @mrArkwright [this](https://scastie.scala-lang.org/GZs4ZVvUSnuNV4y2MW55og) works. [This](https://scastie.scala-lang.org/Ph7DVurcS9u6ATtgccdwnQ) does not. By "trouble coming up with usecase" I meant that you are actually asking me to give you examples when `Foo` would need to know that `A` is `Foo` ... If you don't have a use-case like that, you don't need f-bounded polymorphism. – Dima May 24 '21 at 13:50
  • Oh right, now I see the difference when Bar is not extending Foo. Thanks for the clarification! – mrArkwright May 24 '21 at 14:13
  • I feel like you misread my question regarding the examples. Without the upper bound A is still to be known to be a super type of whatever extends Foo. That enables definitions like `def foo: A = this` which is a use case of f-bounded polymorphism. – mrArkwright May 24 '21 at 14:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232814/discussion-between-mrarkwright-and-dima). – mrArkwright May 24 '21 at 14:16
  • @mrArkwright You are missing the point. We don't (only) want it to be a "supertype of whatever extends Foo". We want it to _also_ be a subtype of `Foo`. That's the whole point. Look at the example in the tutorial you mentioned in your answer. Without the upper bound, you can make `Kitty.renamed` return a 'ClawedCreature` or something. It's not useful, you want it to be a `Kitty`. – Dima May 24 '21 at 14:32
  • Thanks. That was the actual explanation I was looking for! – mrArkwright May 24 '21 at 14:45
  • I should be noted that even with an upper bound nothing prevents us from defining `Kitty.renamed` to return a `ClawedCreature` if we define `ClawedCreature` to extend `Pet[ClawedCreature]`. So the upper bound does provide some additional safety but doesn't eliminate all fallacies of that sort, which is the aforementioned limitation of f-bounded polymorphism in Scala. – mrArkwright May 24 '21 at 14:51
  • @mrArkwright yeah, but at least with the upper bound you guarantee that whatever `rename` returns is still a `Pet` – Dima May 24 '21 at 16:59
  • That's what I meant by _additional safety_. – mrArkwright May 24 '21 at 18:56