Skip to content

Trial: Use with instead of given for context parameters #7973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ yield
### Soft keywords

```
derives extension inline opaque open
as derives extension inline on opaque open
~ * | & + -
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
---
layout: doc-page
title: "Implicit By-Name Parameters"
title: "By-Name Context Parameters"
---

Implicit parameters can be declared by-name to avoid a divergent inferred expansion. Example:
Context parameters can be declared by-name to avoid a divergent inferred expansion. Example:

```scala
trait Codec[T] {
def write(x: T): Unit
}

given intCodec: Codec[Int] = ???
given intCodec as Codec[Int] = ???

given optionCodec[T]: (ev: => Codec[T]) => Codec[Option[T]] {
given optionCodec[T] with (ev: => Codec[T]) as Codec[Option[T]] {
def write(xo: Option[T]) = xo match {
case Some(x) => ev.write(x)
case None =>
Expand All @@ -24,29 +24,29 @@ val s = summon[Codec[Option[Int]]]
s.write(Some(33))
s.write(None)
```
As is the case for a normal by-name parameter, the argument for the implicit parameter `ev`
As is the case for a normal by-name parameter, the argument for the context parameter `ev`
is evaluated on demand. In the example above, if the option value `x` is `None`, it is
not evaluated at all.

The synthesized argument for an implicit parameter is backed by a local val
The synthesized argument for a context parameter is backed by a local val
if this is necessary to prevent an otherwise diverging expansion.

The precise steps for synthesizing an argument for an implicit by-name parameter of type `=> T` are as follows.
The precise steps for synthesizing an argument for a by-name context parameter of type `=> T` are as follows.

1. Create a new given instance of type `T`:
1. Create a new given of type `T`:

```scala
given lv: T = ???
given lv as T = ???
```
where `lv` is an arbitrary fresh name.

1. This given instance is not immediately available as candidate for argument inference (making it immediately available could result in a loop in the synthesized computation). But it becomes available in all nested contexts that look again for an argument to an implicit by-name parameter.
1. This given is not immediately available as candidate for argument inference (making it immediately available could result in a loop in the synthesized computation). But it becomes available in all nested contexts that look again for an argument to a by-name context parameter.

1. If this search succeeds with expression `E`, and `E` contains references to `lv`, replace `E` by


```scala
{ given lv: T = E; lv }
{ given lv as T = E; lv }
```

Otherwise, return `E` unchanged.
Expand All @@ -55,7 +55,7 @@ In the example above, the definition of `s` would be expanded as follows.

```scala
val s = summon[Test.Codec[Option[Int]]](
optionCodec[Int](intCodec)
optionCodec[Int].with(intCodec)
)
```

Expand Down
8 changes: 4 additions & 4 deletions docs/docs/reference/contextual/context-bounds.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ title: "Context Bounds"

## Context Bounds

A context bound is a shorthand for expressing the common pattern of an implicit parameter that depends on a type parameter. Using a context bound, the `maximum` function of the last section can be written like this:
A context bound is a shorthand for expressing the common pattern of a context parameter that depends on a type parameter. Using a context bound, the `maximum` function of the last section can be written like this:
```scala
def maximum[T: Ord](xs: List[T]): T = xs.reduceLeft(max)
```
A bound like `: Ord` on a type parameter `T` of a method or class indicates an implicit parameter `(given Ord[T])`. The implicit parameter(s) generated from context bounds come last in the definition of the containing method or class. E.g.,
A bound like `: Ord` on a type parameter `T` of a method or class indicates a context parameter `with Ord[T]`. The context parameter(s) generated from context bounds come last in the definition of the containing method or class. E.g.,
```scala
def f[T: C1 : C2, U: C3](x: T)(given y: U, z: V): R
def f[T: C1 : C2, U: C3](x: T) with (y: U, z: V) : R
```
would expand to
```scala
def f[T, U](x: T)(given y: U, z: V)(given C1[T], C2[T], C3[U]): R
def f[T, U](x: T) with (y: U, z: V) with C1[T], C2[T], C3[U]) : R
```
Context bounds can be combined with subtype bounds. If both are present, subtype bounds come first, e.g.
```scala
Expand Down
74 changes: 74 additions & 0 deletions docs/docs/reference/contextual/context-functions-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
layout: doc-page
title: "Context Functions - More Details"
---

## Syntax

Type ::= ...
| FunArgTypes ‘?=>’ Type
Expr ::= ...
| FunParams ‘?=>’ Expr

Context function types associate to the right, e.g.
`S ?=> T ?=> U` is the same as `S ?=> (T ?=> U)`.

## Implementation

Context function types are shorthands for class types that define `apply`
methods with context parameters. Specifically, the `N`-ary function type
`T1, ..., TN => R` is a shorthand for the class type
`ContextFunctionN[T1 , ... , TN, R]`. Such class types are assumed to have the following definitions, for any value of `N >= 1`:
```scala
package scala
trait ContextFunctionN[-T1 , ... , -TN, +R] {
def apply with (x1: T1 , ... , xN: TN) : R
}
```
Context function types erase to normal function types, so these classes are
generated on the fly for typechecking, but not realized in actual code.

Context function literals `(x1: T1, ..., xn: Tn) ?=> e` map
context parameters `xi` of types `Ti` to the result of evaluating the expression `e`.
The scope of each context parameter `xi` is `e`. The parameters must have pairwise distinct names.

If the expected type of the context function literal is of the form
`scala.ContextFunctionN[S1, ..., Sn, R]`, the expected type of `e` is `R` and
the type `Ti` of any of the parameters `xi` can be omitted, in which case `Ti
= Si` is assumed. If the expected type of the context function literal is
some other type, all context parameter types must be explicitly given, and the expected type of `e` is undefined.
The type of the context function literal is `scala.ContextFunctionN[S1, ...,Sn, T]`, where `T` is the widened
type of `e`. `T` must be equivalent to a type which does not refer to any of
the context parameters `xi`.

The context function literal is evaluated as the instance creation
expression
```scala
new scala.ContextFunctionN[T1, ..., Tn, T] {
def apply with (x1: T1, ..., xn: Tn) : T = e
}
```
A context parameter may also be a wildcard represented by an underscore `_`. In that case, a fresh name for the parameter is chosen arbitrarily.

Note: The closing paragraph of the
[Anonymous Functions section](https://www.scala-lang.org/files/archive/spec/2.12/06-expressions.html#anonymous-functions)
of Scala 2.12 is subsumed by context function types and should be removed.

Context function literals `(x1: T1, ..., xn: Tn) ?=> e` are
automatically created for any expression `e` whose expected type is
`scala.ContextFunctionN[T1, ..., Tn, R]`, unless `e` is
itself a context function literal. This is analogous to the automatic
insertion of `scala.Function0` around expressions in by-name argument position.

Context function types generalize to `N > 22` in the same way that function types do, see [the corresponding
documentation](../dropped-features/limit22.md).

## Examples

See the section on Expressiveness from [Simplicitly: foundations and
applications of implicit function
types](https://dl.acm.org/citation.cfm?id=3158130).

### Type Checking

After desugaring no additional typing rules are required for context function types.
153 changes: 153 additions & 0 deletions docs/docs/reference/contextual/context-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
layout: doc-page
title: "Context Functions"
---

_Context functions_ are functions with (only) context parameters.
Their types are _context function types_. Here is an example of a context function type:

```scala
type Executable[T] = ExecutionContext ?=> T
```
Context function are written using `?=>` as the "arrow" sign.
They are applied to synthesized arguments, in
the same way methods with context parameters is applied. For instance:
```scala
given ec as ExecutionContext = ...

def f(x: Int): Executable[Int] = ...

f(2).with(ec) // explicit argument
f(2) // argument is inferred
```
Conversely, if the expected type of an expression `E` is a context function type
`(T_1, ..., T_n) ?=> U` and `E` is not already an
context function literal, `E` is converted to an context function literal by rewriting to
```scala
(x_1: T1, ..., x_n: Tn) ?=> E
```
where the names `x_1`, ..., `x_n` are arbitrary. This expansion is performed
before the expression `E` is typechecked, which means that `x_1`, ..., `x_n`
are available as givens in `E`.

Like their types, context function literals are written using `?=>` as the arrow between parameters and results. They differ from normal function literals in that their types are context function types.

For example, continuing with the previous definitions,
```scala
def g(arg: Executable[Int]) = ...

g(22) // is expanded to g((ev: ExecutionContext) ?=> 22)

g(f(2)) // is expanded to g((ev: ExecutionContext) ?=> f(2).with(ev))

g((ctx: ExecutionContext) ?=> f(22).with(ctx)) // is left as it is
```
### Example: Builder Pattern

Context function types have considerable expressive power. For
instance, here is how they can support the "builder pattern", where
the aim is to construct tables like this:
```scala
table {
row {
cell("top left")
cell("top right")
}
row {
cell("bottom left")
cell("bottom right")
}
}
```
The idea is to define classes for `Table` and `Row` that allow
addition of elements via `add`:
```scala
class Table {
val rows = new ArrayBuffer[Row]
def add(r: Row): Unit = rows += r
override def toString = rows.mkString("Table(", ", ", ")")
}

class Row {
val cells = new ArrayBuffer[Cell]
def add(c: Cell): Unit = cells += c
override def toString = cells.mkString("Row(", ", ", ")")
}

case class Cell(elem: String)
```
Then, the `table`, `row` and `cell` constructor methods can be defined
with context function types as parameters to avoid the plumbing boilerplate
that would otherwise be necessary.
```scala
def table(init: Table ?=> Unit) = {
given t as Table
init
t
}

def row(init: Row ?=> Unit) with (t: Table) = {
given r as Row
init
t.add(r)
}

def cell(str: String) with (r: Row) =
r.add(new Cell(str))
```
With that setup, the table construction code above compiles and expands to:
```scala
table { ($t: Table) ?=>

row { ($r: Row) ?=>
cell("top left").with($r)
cell("top right").with($r)
}.with($t)

row { ($r: Row) ?=>
cell("bottom left").with($r)
cell("bottom right").with($r)
}.with($t)
}
```
### Example: Postconditions

As a larger example, here is a way to define constructs for checking arbitrary postconditions using an extension method `ensuring` so that the checked result can be referred to simply by `result`. The example combines opaque aliases, context function types, and extension methods to provide a zero-overhead abstraction.

```scala
object PostConditions {
opaque type WrappedResult[T] = T

def result[T] with (r: WrappedResult[T]) : T = r

def (x: T) ensuring[T](condition: WrappedResult[T] ?=> Boolean): T = {
assert(condition.with(x))
x
}
}
import PostConditions.{ensuring, result}

val s = List(1, 2, 3).sum.ensuring(result == 6)
```
**Explanations**: We use a context function type `WrappedResult[T] ?=> Boolean`
as the type of the condition of `ensuring`. An argument to `ensuring` such as
`(result == 6)` will therefore have a given of type `WrappedResult[T]` in
scope to pass along to the `result` method. `WrappedResult` is a fresh type, to make sure
that we do not get unwanted givens in scope (this is good practice in all cases
where context parameters are involved). Since `WrappedResult` is an opaque type alias, its
values need not be boxed, and since `ensuring` is added as an extension method, its argument
does not need boxing either. Hence, the implementation of `ensuring` is as about as efficient
as the best possible code one could write by hand:

```scala
{ val result = List(1, 2, 3).sum
assert(result == 6)
result
}
```
### Reference

For more info, see the [blog article](https://www.scala-lang.org/blog/2016/12/07/implicit-function-types.html),
(which uses a different syntax that has been superseded).

[More details](./implicit-function-types-spec.md)
Loading