Proposal to Add Implied Instances to the Language

How about **implicit yield**?

Yes. It’s almost as low-level as current way of defining typeclasses instances, while simultaneously is not significantly longer than implied ev of Typeclass[A] { ... }. It’s somewhat higher level because it automatically chooses between lazy val/ object and def.

It has some upsides:

  • it looks familiar to exising Scala programmers, so less impression that Scala 3 is a different language than Scala 2
  • variant implicit ev: Typeclass[A] = new { ... } is generally as flexible as current syntax, so migration would be easy (mechanical) and very localized
  • most importantly - it still makes defining implicit conversions impossible

OK now I understand your justification. There are indeed differences between : and extends.

So in this case I think that we can reuse extends (again, least surprise for existing Scala programmers) when we create a concrete implied instance:

implied ListEq[A] given (A: Eq[A]) extends Eq[List[A]] { 
  // definitions
}

and use : for abstract implied instances:

trait A {
  implied monoid: Monoid[A] 
}

I think that

implied <Instance> given <Evidences> extends <Type>

works pretty well in English.

I cannot predict what people will have a hard time understanding in general, other than looking at my own experience, but I believe instance is much closer to the stated purpose of implicit values:

Implied instance definitions define “canonical” values of given types that serve for synthesizing
arguments to inferable parameters

To me the ideal syntax for beginners would be something like

canonical instance of Ord[Int] { ... }

Of course newcomers will ask “how is this different from a regular instance?”, but I believe the resulting explanation is easy to understand and remember, in part precisely because it allows people to reuse their existing intuition with normal instances. We’re just saying that this instance is very special.

Reading the docs at http://dotty.epfl.ch/docs/reference/contextual, there are a number of different concepts being used, including:

  • contextual abstractions
  • Implied Instances
  • Inferable Parameters
  • Context Bounds
  • Context Queries

It seems that a common theme is context (hence the title “Contextual Abstractions”). This is used in two related but slightly different meanings:

  1. As a domain concept independent of a particular programming language conctruct, where something in the program you are writing is regarded as the “context” for something else, e.g. ExecutionContext.

  2. Particular programming language constructs, i.e. context bounds, context queries.

I assume the latter are named as such due to being meant to support the former.

To me it seems that this would make context a good candiate for a keyword, as in:

context a for B (to be read as “context instance a for B”)

This would have the following advantages:

  • it is both asymmetric and complementary with given, i.e. the parameter is given by the context, or context-given
  • it is coherent with the central theme avoiding introducing additional concepts

The story could be told like this (in informal language): In additional to the normal scope, Scala also has a “context scope”. With a definition like def f given T, the argument of type T will be given by the current context, i.e. the compiler will look up the context scope for an instance of that type. Context instances are put into the context scope by marking them with the keyword context, as in context t for T { }

So we would end up with a smaller number of concepts, all united by a single theme:

  • contextual abstractions
  • context instances
  • context-given parameters/arguments
  • Context Bounds
  • Context Queries

It has one drawback: that “context a for B” could be read as “a is the context for B”. context instance a for B would be more precise, but the former might be an acceptable short-hand.

4 Likes

@kavedaa Proposed something very similar to this in the other thread (Proposal To Revise Implicit Parameters)! The two meanings of context you described align pretty well with the context and derived keywords I proposed there.

Cannot we just omit implicit parameter modifier in implicit definitions? It doesn’t add any information there.

Do we really need a new syntax for inferred terms and not a new kind of definition better suited to express “canonicity” of instances?

Let’s imagine we are able to define objects which are very much like object, but may have parameters. For example, they may have the following restrictions:

  1. They are always final.
  2. They should have stable path.
  3. They doesn’t have any initialization logic (similar to what we have for value classes).
  4. All parameters have singleton types.

Then any application of objects definition may be treated as a singleton type itself. I think it is pretty much desired semantics for typeclass instances in most of the cases. It also gives exact meaning to “canonicity” and decouples it from “inferrability”.

trait Ord[T] {...}

implicit object IntOrd extends Ord[Int] {...}

implicit objects ListOrd[T](elementOrd: Ord[T]) extends Ord[List[T]] {...}

objects Descending[T](implicit ord: Ord[T]) extends Ord[T] {...} 

IntOrd, ListOrd(IntOrd) and Descending(IntOrd) are all typeclass instances, but the last one is not inferrable and should be created explicitly. They are just values and much easier to reason about. Static nature of these values probably make it possible to avoid allocation overhead mentioned in this discussion.

With this new definition we may promote implicit object and implicit objects as a default way to create inferrable typeclass instances and shorten them to just implicit before name.

implicit IntOrd extends Ord[Int] {...}
implicit ListOrd[T](elementOrd: Ord[T]) extends Ord[List[T]] {...}

I understand that this proposal may be much more complicated to spec and implement, but I don’t think we may solve existing problems by just introducing new syntax.

It’s somewhat out of topic here, but I do think there’s value in parameterized object definitions, especially if these objects can be used as stable paths, as in:

object Geometry[N: Numeric] {
  case class Point(x: N, y: N) {
    def sum = x + y
    def map[M: Numeric](f: N => M): Geometry[M].Point =
      Geometry.Point(f(x), f(y))
  }
}

For it to be sound, we’d probably need to ensure no state can be stored in parameterized objects. That, or opt-in memoization of instances if one wants to hold state.

2 Likes

At least for me, “implicit” is a very scannable flag, and removing it would make it much harder to pick out the definitions which are part of the implicit scope.

Changing that to a different (unique) keyword is fine, the mental retraining is worth it for more coherent implicits, but dropping it altogether for a parameterized object seems like a big step backward for readability.

Alas, I have to echo and amplify @sjrd’s objection to the word “instance”.

It is already very difficult to talk about the difference between the type of a class and a particular instantiation of the class in memory. In Java, you call the latter an “object”. But in Scala, object is something else–a unique instantiation of a unique type that also can be associated with a (different) type. So if you say “object” it’s ambiguous. I always try to avoid it.

Luckily there is another widely-used term to refer to the I-have-one case instead of the this-kind-of-thing case: instance! Now we can say that class X {} is a class, and new X is an instance of X. Now we’re about to make that ambiguous too!

So if I can’t call new X an object X because object X is different, and I can’t call it an instance because instance x of Foo is also different, what can I call it?!

This is further compounded by it already being called an instance in many situations, and it not being easily disambiguated from the I-am-the-one-that-can-be-given flavor that we’re seeking to introduce.

So while I love how it reads on its own, due to the context of the rest of the language and how we speak of it I am forced to be strongly against instance as the keyword.

If none of the available suggestions work, it seems that the things that suggest giving or being given have not been discussed here: assume, offer, provide. supply.

6 Likes

If none of the available suggestions work, it seems that the things that suggest giving or being given have not been discussed here: assume , offer , provide . supply .

If you look at it in all required situations, I believe none of the verbs work, in particular aliases look weird.

  assume for Context = localCtx

That does not hang together. We need ideally a noun, but if none is available an adjective would also work.

Sorry, I should have been more explicit. I agree it’s super-awkward with mandatory for as a separator.

assume Context = localCtx
assume Context given Evidence { def ctx = localCtx }
assume x as Context = localCtx
assume x as Context given Evidence { def ctx = localCtx }
2 Likes

I see. However, I feel that not having a mandatory connective like for or as is problematic for generic instances:

assume [T] Context[T] = localCtx
assume [T] Context[T]  given Evidence { def ctx = localCtx }
assume x[T] as Context[T] = localCtx
assume x[T] as Context[T] given Evidence { def ctx = localCtx }

Maybe always require as, nevertheless?

assume as Context = localCtx
assume as Context given Evidence { def ctx = localCtx }
assume x as Context = localCtx
assume x as Context given Evidence { def ctx = localCtx }

assume [T] as Context[T] = localCtx
assume [T] as Context[T]  given Evidence { def ctx = localCtx }
assume x[T] as Context[T] = localCtx
assume x[T] as Context[T] given Evidence { def ctx = localCtx }

… but I still don’t like the alias forms.

2 Likes

I don’t either, but I dislike them less than implied x for Context, and they don’t have the major drawback of instance preventing us from talking about single allocated copies of classes.

1 Like

To throw another color at the bikeshed:

the Context = localCtx
the Context given Evidence = new { def ctx = localCtx }
the Context x = localContext
// or: the x: Context = localContext (doesn't read as nicely though)
the Context x given Evidence = new { def ctx = localCtx }

the [T] Context[T] = localCtx
the [T] Context[T] given Evidence = new { def ctx = localCtx }
the [T] Context[T] x = localCtx
the [T] Context[T] x given Evidence = new { def ctx = localCtx }

Reasons:

  • Unifies syntactically with the[Context[Foo]] used to summon.
  • Three-letter word aligns nicely with val and def.
  • Explicitly describes that the single value of type Context is being defined.

Drawbacks:

  • the is a hugely common word and might not carry enough weight given the importance of the construct.
  • If the had to become a keyword, it would make for a lot of problems

Edit: It would also be nice if the first [T] could be omitted, but I guess that would cause ambiguities if there was already a type T in lexical scope:

the Context[T] given Evidence[T] = new { ... }
// etc

Edit again: Maybe omitting the leading type parameter block could be solved by using similar rules to match types – use a lower-case name for scrutinees:

the Context[t] given Evidence[t] = new { ... }
3 Likes

Call a spade a spade…

How about define...for...given?

define Context[T] = localCtx
define Context[T] given Evidence { def ctx = localCtx }
define x[T] for Context[T] = localCtx
define x[T] for Context[T] given Evidence { def ctx = localCtx }

For me that would be the original

implied..for...given

or my previous suggestion

assumed...for...given

I think the version currently in Dotty pretty much nailed it, it is the best way to express what actually happens here. Assumed might be slightly better, maybe it feels a bit more natural as a kind of a dual to given. But they (assumed and implied) are pretty much the same).

I get the argument against the “for” part. although I don’t see it as a big issue, but if it is, then

assumed as Context = localCtx
assumed as Context given Evidence { def ctx = localCtx }
assumed x as Context = localCtx
assumed x as Context given Evidence { def ctx = localCtx }

assumed [T] as Context[T] = localCtx
assumed [T] as Context[T] given Evidence = new { def ctx = localCtx }
assumed [T] as Context[T] x = localCtx
assumed [T] as Context[T] x given Evidence = new { def ctx = localCtx }

or

implied as Context = localCtx
implied as Context given Evidence { def ctx = localCtx }
implied x as Context = localCtx
implied x as Context given Evidence { def ctx = localCtx }

implied [T] as Context[T] = localCtx
implied [T] as Context[T] given Evidence = new { def ctx = localCtx }
implied [T] as Context[T] x = localCtx
implied [T] as Context[T] x given Evidence = new { def ctx = localCtx }

also works.

I’ve been reading this thread since the beginning, but I still don’t really get what is the problem with “implied”. Now with “for” out of the way, What would still be wrong with the above?

1 Like

I think original proposal that omits assignment operator in typeclass instance definition has some drawbacks.

  1. Implied instance definition looks almost the same as implied alias definition:
implied instanceDefinition for A[B] { ... }
implied aliasDefinition for A[B] = { ... }

Only one non-blank char of difference. Just like with method returning Unit and method with inferred types:

def methodReturningUnit { ... }
def methodWithInferredReturnType = { ... }

Notation for methods without assignment operator was dismissed on many reasons and the imposed alternative is to replace def method { ... } with def method: Unit = { ... } which is 6 non-blank characters longer. Remember that methods returning Unit are quite frequent.

  1. Conflict between abstract implied instances and short class notation.

Objects and classes definitions can omit curly braces if there’s nothing to add or change in object / class definition, i.e. we can write:

class A // {} not needed
class A extends B // {} not needed
object A // {} not needed
object A extends B // {} not needed

If implied instances notations are analogous to objects and classes notations then following lines would mean concrete definitions, not abstract members:

implied A
implied A given B

But then how to define abstract implied instances?

abstract class TypeclassesAggregate {
  implied A // does not look like abstract member
  abstract implied B // well, here 'B' refers in fact to method name, but methods don't need 'abstract' modifier
}

If we always require = (assignment operator) in implicit instances definitions then above problems don’t exist or are reduced.

Once again, there are a lot of assumptions, risks, questionable advantages, and unclear choices. Do we really need this to go into 3.0? It seems to me much safer to let the dust settle on all the clear wins that 3.0 will bring, including any part of this that is straightforward, like allowing to omit the val or def keyword when it’s inferrable, or allowing _ instead of a pointless identifier in more places (maybe including method parameters as well). At that point we’ll be in a much better position to debate changes that alter the language beyond recognition.

In the other thread I mentioned the risks of effect on scala usage metrics but there really are many more risks to consider. Again, I hear the case being made for all the benefits, but risks are usually hard to predict which is why in software you never want to make too many changes at once. Hasn’t our industry learned that lesson enough times yet?

5 Likes

Just to comment more on the underscore thing:

IMO underscore as a word[1] always means the same thing, for a looser definition of meaning: “An identifier would go here if I had one in mind, but I don’t, so whatever.” That said, for a more precise sense of meaning, underscore in imports doesn’t mean the same thing as say in patterns, because in patterns it means “I would put an identifier here to give this thing a name but I won’t use the name anyway so I’ll pass,” while in imports it means “I would put an identifier here if I wanted that specific thing but I want everything.” There are a number of other cases in the first category (see https://stackoverflow.com/a/8001132/333643), but they all convey the same thing. Of course, some of them have different semantics, but they all follow from the context. For instance if you have an expression _ + _ and you read it as “<some thing which I won’t bother naming> + <some other thing which I won’t bother naming>” then it only makes sense as a function with those being its parameters. After all, a function is just an expression with holes punched in it; it’s the usual syntax that obfuscates that, but sometimes it’s necessary (e.g., x => x + x).

Put differently, underscore is always exactly what it looks like, a blank for the the compiler to fill in, and the behavior is usually the only one that makes sense in the context.

From a more technical perspective I can appreciate why people see it as overloading, but unfortunately I think that characterization has negatively affected how Scala is taught.

Anyway, it would be great if there were more places that names were made optional. This has been discussed before. I think it would be useful in a number of places. One example is, sometimes due to designs based on overriding you end up with unused parameters. It would make sense to use _ instead of a name there.

[1] as opposed to when part of an identifier or a number

1 Like