Skip to content

SIP-64: Improve the Syntax of Context Bounds and Givens #81

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

Merged
merged 12 commits into from
Aug 19, 2024

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Mar 11, 2024

No description provided.

That's generally considered too painful to write and read, hence people usually adopt one of two alternatives. Either, eschew context bounds and switch to using clauses:
```scala
def reduce[A](xs: List[A])(using m: Monoid[A]): A =
xs.foldLeft(m)(_ `combine` _)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m.unit

Since we don't have a name for the `Monoid` instance of `A`, we need to resort to `summon` in the body of `reduce`:
```scala
def reduce[A : Monoid](xs: List[A]): A =
xs.foldLeft(summon Monoid[A])(_ `combine` _)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

summon[Monoid[A]].unit

Copy link

@johnynek johnynek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, these are two separate changes that can be considered completely independently:

  1. Context bound changes
  2. Given syntax changes

I think it would be valuable to discuss, implement and approve these two independently.

@JD557
Copy link

JD557 commented Mar 11, 2024

I think something is off in this branch. The Named Tuples commits (from fd964a9 to d649f6e) are included here as well.

I imagine this was intended to only start at 267b8ff?

trait:
def showMax[X : {Ordering, Show}](x: X, y: X): String
class B extends A:
def showMax[X : {Ordering as ordering, Show as show}](x: X, y: X): String =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add a discussion (apologies if I missed it) of the fact that some using/givens don't have a single parameter to dispatch on. For such cases we would still employ the using variant. To me I'd like to see a case that we could completely kill the using variant. To do that, it would be good to have a catalog of examples that don't fit. For instance, does Builder/CanBuildFrom work with this approach? From cats, MonadError won't work with this, I don't think. In any case taking a few such examples like that would strengthen this.

@odersky odersky force-pushed the typeclass-syntax-sip branch from 267b8ff to fe41123 Compare March 11, 2024 15:35
@odersky
Copy link
Contributor Author

odersky commented Mar 11, 2024

@JD557 yes, indeed. I force pushed as an independent separate branch, incorporating fixes to the two typos pointed out by
@johnynek.

The proposal could be split further into two or three independent areas, but there are also connections between the parts:

  • Both context bounds and new givens use as to introduce an optional name.
  • Implementing context bounds for type members relies on deferred givens.

So one logical progression could be (1) deferred givens replacing abstract givens (2) new given syntax (3) context bound changes. But the motivation why the new given syntax is harmonious comes in part from the fact that it is in agreement with names for context bounds.

think we should add a discussion (apologies if I missed it) of the fact that some using/givens don't have a single parameter to dispatch on. For such cases we would still employ the using variant. To me I'd like to see a case that we could completely kill the using variant.

I don't think that's possible or desirable. In my world view type classes are a kind of types for types. That means they can only refer to a single type. Multi-parameter type classes are really constraints passed as context. So "multi-parameter type class" is already a misnomer. The name was invented in Haskell because Haskell does not have a general context passing mechanism, all has to be force-fitted into the typeclass paradigm. And of course there are also bits of context that don't constrain any type parameters. So it seems natural to keep using clauses for these cases, and reserve context bounds for true (that is, single-parameter) type classes.

are time sensitive since they affect existing syntax that was introduced in 3.0, so it's better to make the change at a time when not that much code using the new syntax is written yet.


1. Named tuples are a convenient lightweight way to return multiple results from a function. But the absence of names obscures their meaning, and makes decomposition with _1, _2 ugly and hard to read. The existing alternative is to define a class instead. This does name fields, but is more heavy-weight, both in terms of notation and generated bytecode. Named tuples give the same convenience of definition as regular tuples at far better readability.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this from this SIP?
It's unclear how named tuples are connected with context bounds and givens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, no that was an oversight. Deleted.

@anatoliykmetyuk anatoliykmetyuk changed the title New SIP: Improve the Syntax of Context Bounds and Givens SIP-64: Improve the Syntax of Context Bounds and Givens Mar 12, 2024

- An optional _precondition_, which introduces type parameters and/or using clauses and which ends in `=>`,
- the implemented _type_,
- an optional name binding using `as`,
Copy link
Contributor

@lihaoyi lihaoyi Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the meta-rule here is:

  1. <prefix> as <name> with the name on the right is used where the name is optional and usually elided (import renames, pattern-match name bindings, given names)

  2. <keyword> <name> <suffix> style with the name on the left is used where the name is mandatory and important (val, def, object, class, etc.)

@Kordyjan
Copy link
Contributor

Kordyjan commented Apr 19, 2024

Some random thoughts about the parts of the proposal:

  1. Named Context Bounds:
    The necessity to type summon[...] never bothered me, so I don't see a clear motivation for this change apart from the possibility of achieving the same effect with fewer keystrokes. Also, I really dislike the usage of the as (soft) keyword. I know it is entirely new for Scala, but we cannot ignore that it is used in multiple languages (Rust, Kotlin, C#, and TypeScript come to my mind just now), and in all of them, it means some kind of type-coercion or unchecked cast. What's more, in all of those languages, it has the same general form: <term> as <type>. We want to use it backward as <type> as <term>. This will be highly confusing to any newcomers just discovering Scala. Last, but not least, it breaks the convention that the names of terms always come before their types. I don't like this inconsistency, but I realize it would require advanced syntactic acrobatics to avoid it.

  2. Aggregate Context Bounds:
    That is a very good change; let's approve it.

  3. Expansion of Context Bounds:
    We must check if the change doesn't introduce any weird and non-intuitive type inference. Apart from that, it is an excellent idea.

  4. Context Bounds for Type Members, Deferred GIvens
    Another very good proposal. I was going to side with the opinion that deferred should be a modifier, but the interpretation of it as a compiler-provided macro convinced me.

  5. New Given Syntax
    While I agree that the current given ... : ... with ... syntax is far from perfect, alternatives (except for the trivial cases) are extremely confusing. If I stumbled on given [A : Ord] => Ord[List[A]]: before reading the proposal, I wouldn't even have an idea how to parse it. Named alternatives also suffer from problems with users' expectations about the meaning of the as keyword, as mentioned in point 1.
    I wouldn't change anything as I think there is a smooth transition from:

def lexicographicOrd[T](using Ord[T]): Ord[List[T]] = ???

through

given lexicographicOrd[T](using Ord[T]): Ord[List[T]] = ???

to

 given lexicographicOrd[T](using Ord[T]): Ord[List[T]] with

That makes the last syntax intuitive, even if it is awkward. The proposal doesn't mention alias givens at all, leaving a question about whether it intends to introduce a huge inconsistency between ordinary given instances definitions and alias given definitions.

  1. There is no point 6

  2. Abolish abstract givens
    Very good change, let's approve it.

Copy link
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall 1, 2, 3 and 5 look good to me.

I have serious concerns about 4.

I am sympathetic to 7 (6?) but it's not clear that a library could migrate off of abstract givens without breaking its own binary API.

Comment on lines 194 to 195
**Alternative:** It was suggested that we use a modifier for a deferred given instead of a `= deferred`. Something like `deferred given C[T]`. But a modifier does not suggest the concept that a deferred given will be implemented automatically in subclasses unless an explicit definition is written. In a sense, we can see `= deferred` as the invocation of a magic macro that is provided by the compiler. So from a user's point of view a given with `deferred` right hand side is not abstract.
It is a concrete definition where the compiler will provide the correct implementation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how this can be viewed as a magic macro. Magic or not, a macro is expanded at the call site of the macro. Not somewhere else.

No, this is definitely not a concrete member with a magical body. It is an abstract member that happens to receive some concrete implementation automatically in subclasses.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A non-magic macro is expanded at the use site. A magic macro can avoid that restriction ;-)

The point is, for a deferred given you know it will be (attempted to be) implemented without requiring special provisions for implementers. That's why it is not abstract.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still completely baffled by this line of thought. Magic things are fine when they do things that normal things cannot do, but still where normal things can do them. Example of good magic things:

  • Primitives
  • uninitialized
  • compiletime.ops

The proposed deferred has non-local consequences that go far beyond what we can explain as a macro, even magic. On the contrary, it completely matches what modifiers have the power to do: final has the power to non-locally prevent overriding, for example. deferred as a modifier would be completely in line with other modifiers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking my reasoning further, there are other practical consequences here.

Scaladoc

As a user of a trait containing a deferred given, when I extend it, it is completely different for me if that it is a deferred given or a concrete given with any other body. That means Scaladoc must show the deferred aspect of it. This is not something Scaladoc does for things after =: the part after the = is an implementation detail, not an API. (Unless the thing is an inline itself, but being a macro is completely different than calling a macro.)

Implementation under separate compilation

Likewise, and for the same reason, the compiler also needs to look at the deferred property of things coming from separate compilation. So for practical reasons it also makes no sense for it to be on the implementation side of the =. It is clearly part of the ABI contract (compare ABI here with API in my Scaladoc point).

- Simplification of the language since a feature is dropped
- Eliminate non-obvious and misleading syntax.

The only downside is that deferred givens are restricted to be used in traits, whereas abstract givens are also allowed in abstract classes. But I would be surprised if actual code relied on that difference, and such code could in any case be easily rewritten to accommodate the restriction.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please explain how such code can be "easily rewritten"? I don't think it is easy, or even perhaps possible, to rewrite such code in a way that preserves the binary API of an open class/trait that contains an abstract given.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it would break the binary API. The thing is, I am not sure there is any use case in the wild where abstract givens are used in abstract classes. People usually reach for traits anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, as I mentioned before offline (but I can't find a written trace online), it is simply not acceptable to ever get rid of abstract givens. If there is a library that uses one today, it will never be able to upgrade its compiler version, no matter what source rewritings it attempts to do.

This is a big no-no. Removing them would not only break backward source compat (which we can do), it would break our promises of binary compatibility due to second-order effects.

We can deprecate them, with the usual options to silence the deprecation, but we can never remove them.


- A `given` clause consists of the following elements:

- An optional _precondition_, which introduces type parameters and/or using clauses and which ends in `=>`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you show how using clauses would look like in this context? What about a combination of type parameters and using clauses? All the examples use type parameters only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given [T](using Ord[T]) => Ord[List[T]]:
  ...

```scala
given T = deferred
```
`deferred` is a new method in the `scala.compiletime` package, which can appear only as the right hand side of a given defined in a trait. Any class implementing that trait will provide an implementation of this given. If a definition is not provided explicitly, it will be synthesized by searching for a given of type `T` in the scope of the inheriting class. Specifically, the scope in which this given will be searched is the environment of that class augmented by its parameters but not containing its members (since that would lead to recursive resolutions). If an implementation _is_ provided explicitly, it counts as an override of a concrete definition and needs an `override` modifier.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it awkward that we introduce behavior that is very different between traits and classes. There is no precedent for anything like this in the language. In fact, Scala 3 brought classes and traits closer to each other by allowing constructor parameters in traits.

What if I already have the concrete interpretation in trait? What if I don't have the concrete interpretation in an abstract class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it awkward that we introduce behavior that is very different between traits and classes. There is no precedent for anything like this in the language.

There is precedent. Trait parameters are resolved in the next enclosing class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I still think it is weird but it is acceptable.

```scala
trait Sorted:
type Element
given Ord[Element] = compiletime.deferred
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is my main concern with this proposal. I think this expansion is dangerous and will create compatibility issues. Not backward incompatibility at the language, but intrinsic, synchronous incompatibility between libraries.

The core issue is that this desugaring introduces "nameless" members whose name actually matters a lot. There is no precedent for that in the language.

Consider for example

trait Babar[T]
given Babar[Int] with {}
given Babar[String] with {}

trait BaseA[A] {
  given Babar[A] = deferred
}

trait BaseB[A] {
  given Babar[A] = deferred
}

class Child extends BaseA[Int] with BaseB[String]

Now you need to implement two given_Babar_A that have nothing in common but that happen to share a generated name.

You could argue that this is already problematic with given definitions today, but at least they have an explicit definition. Once we automatically generate given Ord[Element] from type Element : Ord, and then given_Ord_Element from that, there is a multi-step, non-obvious relationship between an innocuous type definition and auto-generated names that clash.

Another example would be

type Foo : {package1.Bar, package2.Bar}

which would immediately result in clashing members. Generalize it to type Foo : package1.Bar in one super trait and type Foo : package2.Bar in another supertrait, and things becomes even more obscure.

There are so many ways that such a scheme is going to go wrong. At the level of the compilation scheme, we're approaching the amount of danger of value classes. Value classes also expose issues that arise in similar situations: where a generic in a superclass cannot be instantiated to some value classes because their bridges double-clash.


Since the core issue is that a generated name acquires a strong semantic meaning, I think we can fix this by disallowing anonymous things here. We could demand that deferred givens have a name, and that includes demanding them for context bounds on type members.

Even then, some issues remain: what is the visibility of the generated member? Does it match the visibility of the type member? What if the type member is package[something] but the implementing class is outside something?

Overall, this proposal introduces for the first time adding invisible members to open traits that actually matter in subtraits and subclasses. We have no precedent for that. There are a zillion issues that could arise from such a new situation, and they need to be explored much more carefully than what the current SIP text suggests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now you need to implement two given_Babar_A that have nothing in common but that happen to share a generated name.

The attempt to provide synthesized implementations should fail with a double definition error in this case. The user can always solve the problem by defining a single given Babar[A] in Child that implements both inherited deferred givens. So there's nothing very new or problematic about it, as far as I can see.

Migration to the new syntax is straightforward, and can be supported by automatic rewrites. For a transition period we can support both the old and the new syntax. It would be a good idea to backport the new given syntax to the LTS version of Scala so that code written in this version can already use it. The current LTS would then support old and new-style givens indefinitely, whereas new Scala 3.x versions would phase out the old syntax over time.


### 7. Abolish Abstract Givens
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be 6?

given Ord[String]:
def compare(x: String, y: String) = ...

given [A : Ord] => Ord[List[A]]:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the arrow mean here?

given Ord[String] as stringOrd:
def compare(x: String, y: String) = ...

given [A : Ord] => Ord[List[A]] as listOrd:
Copy link

@bjornregnell bjornregnell Apr 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the arrow mean here and how does the as bind to it?

Copy link
Contributor Author

@odersky odersky Apr 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as binds to the whole given clause. The => means conditional. If there is an [A: Ord] then here is an Ord[List[A]]. It's role is similar to the arrow in pattern matching. You could also see it as a given that defines a function [A : Ord] to Ord[List[A]]. The two interpretations are the same, so it means that all the usual interpretations of => are applicable and they coincide.

Copy link

@bjornregnell bjornregnell Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the arrow can be used and then it means something different:

Welcome to Scala 3.4.1 (17.0.6, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala> given [T]:(T => String) = x => x.toString
def given_T_to_String[T]: T => String

The first time I saw the new arrow syntax in this thread, I thought it was a given for FunctionN, so there could perhaps be some confusion. How should we relate the new syntax to givens for functions? Have you considered alternatives?

@odersky
Copy link
Contributor Author

odersky commented Apr 19, 2024

@Kordyjan Given clauses are usually written without a name, and that's mostly where the old syntax is weird.

I know it is entirely new for Scala

as is not entirely new, it is already used for import renamings.

@bjornregnell
Copy link

bjornregnell commented May 15, 2024

This proposal is up for the next SIP meeting on May 24th which is soon and I think we should prepare the meeting discussions here.

In general I think the ambition of this SIP is great. I find the first parts of the new syntax and expansion schemes for naming context bounds, multiple context bounds, context bounds for type members reasonable and with support from the compiler to rewrite old syntax and a long grace period before removal I think it is doable.

However, the controversial/"risky" part of this proposal is the change of syntax for givens, and the main concern is language stability versus language improvement. Or in other words: is the improvement to givens worth the costs of migration and potential risk of decreased willingness to go for Scala Next?

@odersky It would be great if the below issues can be clarified in the proposal or in this comment thread, as input to the meeting:

  1. If we introduce a new syntax for givens, how would the deprecation scheme relate to the LTS roadmap? Would the current 3.3-LTS given syntax still available in the next 3.x-LTS ? (Will 3.7 be then next LTS?)
  2. What parts of the new syntax for given can be back-ported to 3.3-LTS so that the ones stuck on that LTS still can use the new syntax in parallell with the old syntax? (Is it right to assume from the current text that all changes can be back-ported?)
  3. I'd like an overview of the different kinds of givens in a before-after-table or similar, as I think it is still difficult to get the big picture of all the kinds of given and the different syntax variants over time.

Assuming something like (or similar):

case class MyType(i: Int)

case class MyGenType[A](a: A)

trait MyOldTypeClass[T]:
  ...

trait MyNewTypeClass:
  type Self
  ...

I think it would be great if you include a summary overview for the proposed new given syntax, perhaps like this (or similar) in the proposal:

kind of given current proposal
named given value given x: MyType = MyType(42) given MyType as x = MyType(42)
anonymous given value given MyType = MyType(42) no change
...etc.

As there are many types of givens, some available here, I think it is valuable if there is a such a systematic walk-through of all combinations so we don't miss any feature interaction and also can get an overview of how big the syntax changes are. In the current proposal only a few of all possible kinds of givens are shown in the new syntax and it is not clear to me if more variants are impacted.

I think the new arrow syntax using => is the most controversial/radical change so its important to include explanations of interpretations/expansions for that in the actual proposal (some rationale is currently available in the review comment only). I find this rather difficult to read: given [A : Ord] => Ord[List[A]]: ... and I wonder if a keyword instead of => such as for or similar could make it easier to grok? Have you considered alpha keyword alternatives instead of a symbol?

@bjornregnell
Copy link

Another question I have:

The only downside is that deferred givens are restricted to be used in traits, whereas abstract givens are also allowed in abstract classes.

Can deferred givens be allowed also in abstract classes? If no, why not?

@odersky
Copy link
Contributor Author

odersky commented May 20, 2024

Can deferred givens be allowed also in abstract classes? If no, why not?

It would complicate things a bit and we would lose flexibility. We want to have a strict separation of classes/traits where deferred givens are defined and where they are implemented. If deferred givens can be defined in abstract classes, then they would in turn not be implemented in abstract classes. But we sometimes might want to implement deferred givens or context bounds in classes that are still missing the implementations of some members.

We also have precedent for this split: trait parameters. These are defined in traits and the corresponding arguments must appear in the first implementing class, where it does not matter whether that class is abstract or not.

@odersky
Copy link
Contributor Author

odersky commented Jul 11, 2024

The discussion on contributors gave a clear preference for

  • expressing optional names using prefix x: instead of suffix as x.
  • using the alternative syntax that makes givens more like functions.

I propose to change the SIP to reflect these preferences as the standard proposal and to change the implementation accordingly. Are there any objections to this?

@Ichoran
Copy link

Ichoran commented Jul 11, 2024

I am happy with it though I suspect it is not the better solution for most people due to lesser familiarity with curried function syntax than with method syntax.

Because the : ... with problem still exists, except : ... with got renamed => ... : (explanation), I would also change the discussion of with. You still have something that looks like it is in type position but isn't a type except a new type is synthesized for it so it is a type after all albeit one you can't name yourself but can see when it's inferred (see discussion in text).

In

given myOrdList: [A] => Ord[A] => Ord[List[A]]:
  def compare...

what is that first : after myOrdList? The thing on the right is, as the discussion says, not a type ascription.

If we don't want to do anything about this, the discussion with with can simply say that since we open anonymous class blocks via : not with (i.e. new X: not new X with), we want to parallel that here.

@bishabosha
Copy link
Member

bishabosha commented Jul 12, 2024

Edit: ignore - I proposed to drop the initial colon after name, but I think there are too many ambiguities

@sideeffffect
Copy link

using the alternative syntax that makes givens more like functions.

How would the syntax look like if one wants to name (one of the) dependencies?

A

  given listOrd: [A] => (aOrd: Ord[A]) => Ord[List[A]]:
    ...

or

B

  given listOrd[A](using aOrd: Ord[A]): Ord[List[A]]:
    ...

I'm asking, because A feels a bit foreign/alien to me, not like anything in Scala. On the other hand, B looks fine IMHO, it's very similar to a method definition which even Scala newbies are comfortable with.

Please disregard my comment if it's too bike-sheddy for too late. But I was curious, what's the latest decision.


For instance, type and val above would expand to
```scala
type Comparer = [X] => (x: X, y: X) => Ord[X] ?=> Boolean

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this right? to me, [X: Ord] => (X, X) => Boolean should expand to [X] => Ord[X] ?=> (X, X) => Boolean. Why is the context function getting interwoven in the middle of (X, X) => Boolean?

@johnynek
Copy link

The proposal still has examples like:

given [A: Ord] => Ord[List[A]]:
  def compare(x: List[A], y: List[A]) = ???

to me, a very important concept in programming is the substitution principle. When I look at this, I want to be able to leverage substitution:

// this type should be well defined as [A] => Ord[A] ?=> Ord[List[A]]
type OrdList = [A: Ord] => Ord[List[A]]

// and we should be able to use it in a given:
given OrdList:
  def compare(x: List[A], y: List[A]) = ...

// or with a value:

val myOrder: [A] => Ord[A] ?=> Ord[List[A]] = ...

given OrdList:
  myOrder

Are those substitutions valid?

@odersky
Copy link
Contributor Author

odersky commented Jul 15, 2024

How would the syntax look like if one wants to name (one of the) dependencies?

It's (A)

[A] => (aOrd: Ord[A]) => Ord[List[A]]:

And yes, that's a legal function type in Scala. It's already used as the syntax for a dependent function type such as

(x: C) => x.Elem

@odersky
Copy link
Contributor Author

odersky commented Jul 15, 2024

@johnynek

Are those substitutions valid?

They are not. The arrow really means implication. The analogy with function types serves as a help to remember the syntax, not to be taken literally. I argue this is not a problem since nobody should want to define givens for functions, that's generally a very dubious thing to do. If you really want to implement a give instance of a function type, you can use the type alias as you have shown, or put the type in parentheses.

@johnynek
Copy link

In my opinion, defining a given context function seems pretty natural. In fact, that is what we are doing with given [A: Ord] => Ord[List[A]] in my opinion. So, I would rather simplify the number of contexts and say that is exactly what we are doing in this case.

This proposal contains the revised syntax for givens which was discussed here and on
contributors.

# Conflicts:
#	content/typeclasses-syntax.md
@odersky
Copy link
Contributor Author

odersky commented Jul 18, 2024

In my opinion, defining a given context function seems pretty natural. In fact, that is what we are doing with given [A: Ord] => Ord[List[A]] in my opinion.

Yes, that's certainly the inspiration. But the fine print does not work out. Given parameters can carry modifiers or annotations but function parameters can't. That's why I was careful to write "it should look like a function type", not "it should be a function type". Still, I think the analogy with function types is useful for learning and intuition. Most people won't even notice the details where they differ.

@odersky
Copy link
Contributor Author

odersky commented Jul 18, 2024

@Ichoran, all: I did a major update of the proposal which now puts the revised syntax first.

@bjornregnell
Copy link

bjornregnell commented Jul 22, 2024

Given parameters can carry modifiers or annotations but function parameters can't.

Hmmm.
@odersky These implicit function values do work with both a modifier (final, inline) and an annotation (at unchecked):

Welcome to Scala 3.4.2 (21.0.3, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                    
scala> final given f: (Int => Int) = x => x + 1
lazy val f: Int => Int

scala> @unchecked given f: (Int => Int) = x => x + 1
lazy val f: Int => Int
                                                                                    
scala> inline def z(using inline f: Int => Int) = f(0)
def z(using f: Int => Int): Int

scala> def z(using @unchecked f: Int => Int) = f(0)
def z(using f: Int => Int): Int

scala> z
val res0: Int = 1
                                   

@odersky
Copy link
Contributor Author

odersky commented Jul 23, 2024

@bjornregnell I was referring to parameters not the given definitions themselves. For instance this is OK:

given (@unchecked x: T) => U = ...

But this is not:

val v: (@unchecked x: T) => U = ...

Here, (x: T) => U is a dependent function type, and these don't permit modifiers or annotations on their parameters.

@bjornregnell
Copy link

bjornregnell commented Jul 23, 2024

Aha. Thanks for clarification. @odersky

So then I guess it would be an opportunity, when given function values are wanted, to first make a normal def (with all the possible glory of modifiers and annotations etc) and then use it as an eta-expanded value in the body of an alias given like so:

Welcome to Scala 3.4.2 (17.0.8.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                
scala> inline transparent def f(@unchecked inline x: Int) = x
def f(x: Int): Int
                                                                                
scala> given (Int => Int) = f
lazy val given_Int_to_Int: Int => Int
                                                                                
scala> def z(using f: Int => Int) = f(0)
def z(using f: Int => Int): Int
                                                                                
scala> z
val res0: Int = 0

@odersky
Copy link
Contributor Author

odersky commented Jul 24, 2024

@bjornregnell Yes, that's what I would recommend in this case as well.

1. A simple typeclass instance, such as `Ord[Int]`.
2. A parameterized type class instance, such as `Ord` for lists.
3. A type class instance with an explicit context parameter.
4. A type class instance with a named eexplicit context parameter.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "eexplicit" -> explicit

@kyouko-taiga kyouko-taiga merged commit da6529f into scala:main Aug 19, 2024
@bjornregnell
Copy link

This SIP is up for vote on Friday Sep 27th. As one of the assignees I hereby state my recommendation to the Committee:

After discussions and feedback here on Contributors and also updates to the proposal accordingly, I hereby recommend to vote Yes, as I find that the pros of improvements in ergonomics and regularity outweigh the cons of changing the given-syntax. This also paves the way for coming SIP:s of improvement of type class ergonomics.

@bjornregnell
Copy link

@sjrd @Kordyjan What are your recommedations to the voters?

@sjrd
Copy link
Member

sjrd commented Sep 27, 2024

I recommend to vote yes as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.