Updated Proposal: Revisiting Implicits

My perception of boiler plate for defining typeclass implementations are:

  1. it doesn’t say/indicate what it is. I.e. the programmer’s intent is unclear. For comparison this isn’t the case with OO classes, where the programmer’s intent is clearly identified by keywords such as “class, object, trait, extends, etc”.
  2. context bounds - my understanding is that these are only syntactic sugar and predominantly useful for typeclasses instances.
  3. the object definition - I look at that, and think to myself, what on earth is this for?
  4. that it apparently requires more complex extension syntax - Martin indicated that collective extension syntax alone isn’t sufficient because of type class definitions, so it seems to me that the type class definition makes other parts of the language more complex as well.

For me, I perceive that the complexity of the type classes definition might be high enough that it would still make my code incomprehensible for a normal developer to understand (I.e. perhaps someone smart who has a background in C, Java, Python, etc, but not Haskell or other functional languages).

There are aspects of Scala that I really love, and many of the features in Scala 3 look great. But I have to be honest, despite writing Scala as a hobby for the last 10ish years, I am really wondering whether it isn’t pragmatically the right language for me, and perhaps I would prefer programming in Rust, Go, or even just Java instead (which is gradually adding more FP constructs). The beginner surface of Scala seems simple, but with a really deep underlying complexity, I was really hoping that Scala 3 was going to be a simpler language than Scala 2, but that is not my perception from reading some of the dotty documentation - if anything it feels even more complex.

I could be completely wrong, but I think the FP experts will like Scala 3, although they might prefer Haskell or other pure FP language. But I do wonder what companies, currently using Scala 2 and perhaps already concerned about complexity, will think of Scala 3?

I apologize if this comes across as a rant, and who am I to complain, given that I’m not doing any of the work updating the language, which I also appreciate is really hard. But hopefully feedback from someone who is predominantly concerned about readability for non-experts is still helpful in some way.

3 Likes

While I agree that, in that greatly simplified example, the only boilerplate is the apply method, most typeclasses need a bit more plumbing to be user friendly.

For example, while the exact structure would be different in Scala 3, there’s useful functionality in the simulacrum readme that isn’t expressed in the new typeclass docs - of particular interest are measures to decrease the import tax for users of the typeclass.

1 Like

Concrete examples would be nice. If your typeclass contains extension methods, you don’t need to import them separately, they are in scope if an implicit of the typeclass is in scope, so no import tax there. The other common cause of import tax is the tendency to not put typeclass instances in the companion of the type class (and therefore it’s default implicit scope), this happens in cats but hopefully will change in the future: Implicit scope and Cats

1 Like

That has not been my experience.

For example, the only way I could consistently get the extension methods for this Convertible typeclass to compile was to use the given ops[A]: AnyRef { ... } style of defining extension methods, and explicitly import them using import types.Convertible.given in each file where I wanted to use them.

1 Like

I am sure that the complexity high enough that it is used only by core library writers in our company today.
if library users should know something about type classes I will prefer not to use these.
We have very strong requirements to simplicity of our business code.

It seems it does not matter for me how simple(or difficult) it is to write typeclasses.
It is not problem at all to write typeclasses in scala 2 for main types.
But I think while additional typeclasses lead to orphan tasks which have no good decision because there are lack of scope reusage via aggregation. Typeclasses will never be broadly used in our company.
There is a complexity to accustom to additional typeclasses which does not cost code conciseness.

https://scastie.scala-lang.org/RmDWCmBDQqKG0ZXR0HSsDA looks pretty good to me compared to the simulacrum example.

Is given a hard keyword or soft keyword? I don’t remember seeing the import X.given syntax but while it makes sense, I don’t see how it can be backwards-compatible.

See http://dotty.epfl.ch/docs/reference/contextual/extension-methods-new.html for details, in particular:

An extension method is applicable if it is a member of some given instance at the point of the application.

Concrete examples would be nice. Perhaps it was a compiler bug that has since been fixed, but that’s not how it was behaving when I wrote that code (first few weeks of Dec 2019)

There’s a concrete example right in https://dotty.epfl.ch/docs/reference/contextual/typeclasses-new.html, notice how the extension method combine is available in sum without any import ? This works because a given Monoid[T] is in scope.

TBH, I don’t know the processes around the code samples in the documentation, so I don’t know if those are examples which are intended to be representative of how things are spec’d to work eventually, or if they’re guaranteed to compile, so I don’t really consider them concrete examples. I assume you didn’t consider the examples in the simulacrum documentation “concrete” for similar reasons - which is fair, and why I didn’t mind providing something I’ve written personally.

It’s also not clear this will work as advertised when they’re defined in separate files, or what the imports would need to look like. It’s entirely possible that it didn’t work for me because I screwed up the imports in some way, but I re-wrote the way the extension methods were handled several times before I found something stable, and at that point I was willing to pay the import tax just to get the thing working.

Did you see the scastie above? Updated Proposal: Revisiting Implicits

FWIW, the final code from https://www.slideshare.net/pjschwarz/scala-3-by-example-better-semigroup-and-monoid

@main def main =

  trait Semigroup[A] with
    def combine(l: A, r: A): A
    def (l: A) |+| (r: A): A = combine(l, r)
    def (as: Seq[A]) combineAllOption: Option[A] = 
      as.reduceOption(_ |+| _)
    def combineOption(as: A*): Option[A] = 
      combineAllOption(as)

  object Semigroup with
    def apply[A](given Semigroup[A]) = summon[Semigroup[A]]

  given Semigroup[Int] with
    def combine(l: Int, r: Int): Int = l + r

  given Semigroup[String] with
    def combine(l: String, r: String): String = l + r

  assert( (2 |+| 3) == 5 )
  assert( ("2" |+| "3") == "23" )
  assert( Semigroup[Int].combineOption() == None)
  assert( Semigroup[Int].combineOption(2,3,4) == Some(9))
  assert( Semigroup[String].combineOption("2","3","4") == Some("234"))
  assert( List[Int]().combineAllOption == None)
  assert( List(2,3,4).combineAllOption == Some(9))
  assert( List("2","3","4").combineAllOption == Some("234"))

  trait Monoid[A] extends Semigroup[A] with
    def unit: A
    def (as: Seq[A]) combineAll: A = 
      as.foldLeft(unit)(_ |+| _)
    def combine(as: A*): A = 
      as.combineAll

  object Monoid with
    def apply[A](given Monoid[A]) = summon[Monoid[A]]

  given Monoid[Int] with
    def combine(l: Int, r: Int): Int = l + r
    def unit: Int = 0

  given Monoid[String] with
    def combine(l: String, r: String): String = l + r
    def unit: String = ""

  assert( (2 |+| 3 |+| Monoid[Int].unit) == 5 )
  assert( ("2" |+| "3" |+| Monoid[String].unit) == "23" )
  assert( Monoid[Int].combine() == Monoid[Int].unit )
  assert( Monoid[Int].combine(2,3,4) == 9)
  assert( Monoid[String].combine("2","3","4") == "234")
  assert( List[Int]().combineAll  == Monoid[Int].unit )
  assert( List(2,3,4).combineAll  == 9 )
  assert( List("2","3","4").combineAll == "234" )
1 Like

Yes, though as Semigroup was in the same scope as where it was being used, I didn’t realize the point you were trying to make was more than just “this looks reasonable to write”.

I’ve tweaked it so that this is not the case (Scastie - An interactive playground for Scala.), and it looks like it works for this example. I’ll have to go back and see if it works in the more complicated case.

Unfortunately, I only got a few slides in before the color choices started to make my head hurt. Apparently, this was intentional

@morgen-peschke suffer no more :grinning: - just skip to the code in the last slide, which is provided above, both in the colour scheme chosen by this site, and as a scastie in which you can select the ‘pastel colours on a white background’ look-and-feel.

What about a selection rule for type-parameters instead, as showcased in the context-applied compiler plugin?

def fn[F[_]: Applicative: Traverse, G[_]: Applicative]: G[F[Int]] = 
  F.traverse(F.pure(""))(s => G.pure(s.size))

def fn[F[_]: Applicative: Traverse, G[_]: Applicative] : G[F[Int]] = 
  summon[Traverse[F]].traverse(summon[Applicative[F]].pure(""))(s => summon[Applicative[G]].pure(s.size))
2 Likes

While this is true, keep in mind that it’s syntax sugar that many of us use heavily – they’re pretty central to even mildly FP-flavored Scala. Losing them would be a significant additional change for a huge amount of existing code, and would force people to change more habits, increasing the upgrade tax, for no particularly good reason…

3 Likes

Why mark trait as typeclass? Shouldn’t trait Monad[F[_]] extends Functor [F] suffice?

Note, I have happily used aspects of a functional programming style in Scala for many years without any knowledge that context bounds even existed as a thing :slight_smile:

I’m not advocating removing context bounds to make folks lives harder, that would be unreasonable.

But, if there a bit more syntactic sugar for defining type class instances, then it might be context bounds aren’t needed, or are only needed in a few corner case scenarios. If this ended up being the case then I would recommend that context bounds are deprecated and eventually removed, with existing users required to migrate to the equivalent long hand in those corner cases (which presumably could also be done via a rewrite script). My justification is only to simplify the language, particularly if it was to remove an obscure feature that is no longer used very much.

You may wonder why I think that it is reasonable to remove “context bounds” but “add new typeclass instance”, and for me there are two key differences:

  1. It would be easy for someone without full knowledge of the language to google for something like “scala typeclass instance”, but I suspect that it is much harder to google solely based on context bound syntax, without already knowing that it is called, even more so if “:” is given further meaning in Scala 3.
  2. If the syntax for typeclasses is lightweight enough then I think that it will be more heavily used in regular code, and hence naturally become more familiar to regular scala developers.

Of course, if context bound syntax is still frequently needed even if scala had syntactic sugar for typeclass instance definitions then by all means keep it. But be aware that it is an additional complexity to the language that is probably not intuitive to non Scala experts, and I see that as a negative to the long term health of the language.

1 Like

I still don’t understand fully what your proposal is. Do you propose adding “typeclass instance” as an alias for “given” if the declaration of the implementing class of the type of the given is marked with a “typeclass” keyword?

You say that in OOP patterns are normally marked with keywords too, but I’ve never seen that. What language has that?