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
given
s 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 List
s.
- There is no need to pass the
given
s 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.
given ... as ...
syntax? I'm somehow struggling to find any mention of it anywhere? – Sheehyas
keyword in the syntax as experimental? I.e.given ... with ...
is part of Scala 3 butgiven ... as ... with
is not? – Prowgiven ... as ... with:
does not exist. Specifically theas
and the:
afterwith
. – Ai:
afterwith
is totally a typo. Removed from the original post now! – Prow