Scala3 "as" and "with" keywords used with "given"
Asked Answered
P

2

7

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...

Prow answered 3/12, 2021 at 10:45 Comment(7)
Where did you find the given ... as ... syntax? I'm somehow struggling to find any mention of it anywhere?Sheehy
Neither the documentation nor the artima book mentions it.Sheehy
@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.Prow
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.Ai
@Ai 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?Prow
@Prow Yes AFAIK given ... as ... with: does not exist. Specifically the as and the : after with.Ai
Sorry, the : after with is totally a typo. Removed from the original post now!Prow
S
13

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.
Sheehy answered 3/12, 2021 at 11:33 Comment(2)
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...Prow
@Prow 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.Sheehy
M
1

This confusing syntax is called out even by Prof. Odersky (It's not just you):

A good language syntax is like a Bach fugue: A small set of motifs is combined in a multitude of harmonic ways. Dissonances and irregularities should be avoided.

When designing Scala 3, I believe that, by and large, we achieved that goal, except in one area, which is the syntax of givens. There are some glaring dissonances, as seen in this code for defining an ordering on lists:

given [A](using Ord[A]): Ord[List[A]] with
  def compare(x: List[A], y: List[A]) = ...

The : feels utterly foreign in this position. It’s definitely not a type ascription, so what is its role? Just as bad is the trailing with. Everywhere else we use braces or trailing : to start a scope of nested definitions, so the need of with sticks out like a sore thumb.

We arrived at that syntax not because of a flight of fancy but because even after trying for about a year to find other solutions it seemed like the least bad alternative. The awkwardness of the given syntax arose because we insisted that givens could be named or anonymous, with the default on anonymous, that we would not use underscore for an anonymous given, and that the name, if present, had to come first, and have the form name [parameters] :. In retrospect, that last requirement showed a lack of creativity on our part.

Sometimes unconventional syntax grows on you and becomes natural after a while. But here it was unfortunately the opposite. The longer I used given definitions in this style the more awkward they felt, in particular since the rest of the language seemed so much better put together by comparison. And I believe many others agree with me on this. Since the current syntax is unnatural and esoteric, this means it’s difficult to discover and very foreign even after that. This makes it much harder to learn and apply givens than it need be.

What a poet. I always love a person who's both wise & humble.

Anyways, this area is actively being worked on. There are some mentions of an as keyword in there possibly existing in the future, but just that: mentions.

Source: https://contributors.scala-lang.org/t/pre-sip-improve-syntax-for-context-bounds-and-givens/6576


P.S. 2023.04.25 - iiuc --- referencing @Andrey Tyukin 's answer --- SIP-64 proposes:

// step 3
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
}
// step 3.5
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
}
// step 4 [NEW]
given [A] => Foo[List[A]] as listFoo {
  // I wonder how syntax highlighting errors are handled by SO... xD
  def foo(a: List[A], b: List[A]): List[A] = a ++ b
  def bar: List[A] = Nil
}
// step 5 [NEW]
given [A] => Foo[List[A]] {
  def foo(a: List[A], b: List[A]): List[A] = a ++ b
  def bar: List[A] = Nil
}

But it also does way more powerful stuff.

Lovely.


EDIT 1: 2023.06.11. Status report; It looks like there is now an experiment implementing this as part of the Scala 3.5.1 release candidate by the name of "modularity". 🤷

Marroquin answered 25/4 at 20:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.