5

Currently learning about Scala 3 implicits but I'm having a hard time grasping what the ​as and with keywords do in a definition like this:

given listOrdering[A](using ord: Ordering[A]) as Ordering[List[A]] with
 ​def compare(a: List[A], b: List[A]) = ...

I tried googeling around but didn't really find any good explanation. I've checked the Scala 3 reference guide, but the only thing I've found for as is that it is a "soft modifier" but that doesn't really help me understand what it does... I'm guessing that as in the code above is somehow used for clarifying that listOrdering[A] is an Ordering[List[A]] (like there's some kind of typing or type casting going on?), but it would be great to find the true meaning behind it.

As for with, I've only used it in Scala 2 to inherit multiple traits (class A extends B with C with D) but in the code above, it seems to be used in a different way...

Any explanation or pointing me in the right direction where to look documentation-wise is much appreciated!

Also, how would the code above look if written in Scala 2? Maybe that would help me figure out what's going on...

greenTea
  • 204
  • 2
  • 7
  • Where did you find the `given ... as ...` syntax? I'm somehow struggling to find any mention of it anywhere? – Andrey Tyukin Dec 03 '21 at 10:51
  • Neither [the documentation](https://docs.scala-lang.org/scala3/reference/contextual/givens.html) nor the artima book mentions it. – Andrey Tyukin Dec 03 '21 at 10:57
  • 1
    @AndreyTyukin I'm taking the Coursera course Functional Programming Design in Scala, and there I encountered this syntax but unfortunately the "as" and "with" keywords weren't explained. The course uses Scala 3. – greenTea Dec 03 '21 at 12:16
  • 5
    This is syntax that was experimented with in the past but is not part of Scala 3. If that's really in the Coursera course you may want to report that to the maintainer of that course. – Jasper-M Dec 03 '21 at 12:49
  • @Jasper-M that would explain why I couldn't find anything about it in the official Scala 3 documentation (although the documentation is still in progress, so there was no way for me to figure this out myself...). Just to be extra clear, are you referring to the `as` keyword in the syntax as experimental? I.e. `given ... with ...` is part of Scala 3 but `given ... as ... with` is not? – greenTea Dec 03 '21 at 12:57
  • 1
    @greenTea Yes AFAIK `given ... as ... with:` does not exist. Specifically the `as` and the `:` after `with`. – Jasper-M Dec 03 '21 at 14:12
  • Sorry, the `:` after `with` is totally a typo. Removed from the original post now! – greenTea Dec 03 '21 at 14:35

1 Answers1

10

The as-keyword seems to be some artifact from earlier Dotty versions; It's not used in Scala 3. The currently valid syntax would be:

given listOrdering[A](using ord: Ordering[A]): Ordering[List[A]] with
 ​ def compare(a: List[A], b: List[A]) = ???

The Scala Book gives the following rationale for the usage of with keyword in given-declarations:

Because it is common to define an anonymous instance of a trait or class to the right of the equals sign when declaring an alias given, Scala offers a shorthand syntax that replaces the equals sign and the "new ClassName" portion of the alias given with just the keyword with.

i.e.

given foobar[X, Y, Z]: ClassName[X, Y, Z] = new ClassName[X, Y, Z]:
  def doSomething(x: X, y: Y): Z = ???

becomes

given foobar[X, Y, Z]: ClassName[X, Y, Z] with
  def doSomething(x: X, y: Y): Z = ???

The choice of the with keyword seems of no particular importance: it's simply some keyword that was already reserved, and that sounded more or less natural in this context. I guess that it's supposed to sound somewhat similar to the natural language phrases like

"... given a monoid structure on integers with a • b = a * b and e = 1 ..."

This usage of with is specific to given-declarations, and does not generalize to any other contexts. The language reference shows that the with-keyword appears as a terminal symbol on the right hand side of the StructuralInstance production rule, i.e. this syntactic construct cannot be broken down into smaller constituent pieces that would still have the with keyword.


I believe that understanding the forces that shape the syntax is much more important than the actual syntax itself, so I'll instead describe how it arises from ordinary method definitions.

Step 0: Assume that we need instances of some typeclass Foo

Let's start with the assumption that we have recognized some common pattern, and named it Foo. Something like this:

trait Foo[X]:
  def bar: X
  def foo(a: X, b: X): X

Step 1: Create instances of Foo where we need them.

Now, assuming that we have some method f that requires a Foo[Int]...

def f[A](xs: List[A])(foo: Foo[A]): A = xs.foldLeft(foo.bar)(foo.foo)

... we could write down an instance of Foo every time we need it:

f(List(List(1, 2), List(3, 4)))(new Foo[List[Int]] {
  def foo(a: List[Int], b: List[Int]) = a ++ b
  def bar: List[Int] = Nil
})
  • Acting force: Need for instances of Foo
  • Solution: Defining instances of Foo exactly where we need them

Step 2: Methods

Writing down the methods foo and bar on every invocation of f will very quickly become very boring and repetitive, so let's at least extract it into a method:

def listFoo[A]: Foo[List[A]] = new Foo[List[A]] {
  def foo(a: List[A], b: List[A]): List[A] = a ++ b
  def bar: List[A] = Nil
}

Now we don't have to redefine foo and bar every time we need to invoke f; Instead, we can simply invoke listFoo:

f(List(List(1, 2), List(3, 4)))(listFoo[Int])
  • Acting force: We don't want to write down implementations of Foo repeatedly
  • Solution: extract the implementation into a helper method

Step 3: using

In situations where there is basically just one canonical Foo[A] for every A, passing arguments such as listFoo[Int] explicitly quickly becomes tiresome too, so instead, we declare listFoo to be a given, and make the foo-parameter of f implicit by adding using:

def f[A](xs: List[A])(using foo: Foo[A]): A = xs.foldLeft(foo.bar)(foo.foo)

given listFoo[A]: Foo[List[A]] = new Foo[List[A]] {
  def foo(a: List[A], b: List[A]): List[A] = a ++ b
  def bar: List[A] = Nil
}

Now we don't have to invoke listFoo every time we call f, because instances of Foo are generated automatically:

f(List(List(1, 2), List(3, 4)))
  • Acting force: Repeatedly supplying obvious canonical arguments is tiresome
  • Solution: make them implicit, let the compiler find the right instances automatically

Step 4: Deduplicate type declarations

The given listFoo[A]: Foo[List[A]] = new Foo[List[A]] { looks kinda silly, because we have to specify the Foo[List[A]]-part twice. Instead, we can use with:


given listFoo[A]: Foo[List[A]] with
  def foo(a: List[A], b: List[A]): List[A] = a ++ b
  def bar: List[A] = Nil

Now, there is at least no duplication in the type.

  • Acting force: The syntax given xyz: SomeTrait = new SomeTrait { } is noisy, and contains duplicated parts
  • Solution: Use with-syntax, avoid duplication

Step 5: irrelevant names

Since listFoo is invoked by the compiler automatically, we don't really need the name, because we never use it anyway. The compiler can generate some synthetic name itself:

given [A]: Foo[List[A]] with
  def foo(a: List[A], b: List[A]): List[A] = a ++ b
  def bar: List[A] = Nil
  • Acting force: specifying irrelevant names that aren't used by humans anyway is tiresome
  • Solution: omit the name of the givens where they aren't needed.

All together

In the end of the process, our example is transformed into something like

trait Foo[X]:
  def foo(a: X, b: X): X
  def bar: X

def f[A](xs: List[A])(using foo: Foo[A]): A = xs.foldLeft(foo.bar)(foo.foo)

given [A]: Foo[List[A]] with
  def foo(a: List[A], b: List[A]): List[A] = a ++ b
  def bar: List[A] = Nil


f(List(List(1, 2), List(3, 4)))
  • There is no repetitive definition of foo/bar methods for Lists.
  • There is no need to pass the givens explicitly, the compiler does this for us.
  • There is no duplicated type in the given definition
  • There is no need to invent irrelevant names for methods that are not intended for humans.
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • Thanks for a very nice summary of the reasoning behind the syntax. However, my question was specifically about the keywords and what they mean in this particular context. And I still don't really get that from your explanation. From what you wrote, it seems that `A with { def foo= ... }` is just syntactic sugar for `trait B { def foo; }; A = new B { def foo = ...}` But does `with` only work like this when `A` is a `given`? Is this described somewhere in the documentation? And the `as` keyword still puzzles me... – greenTea Dec 03 '21 at 12:34
  • @greenTea Updated. I think you're trying to interpret too much into it. It's really just a reserved keyword: it's only purpose is to separate the code to the left of it from the code to the right of it. – Andrey Tyukin Dec 03 '21 at 20:29