Even less explicit typing on def implementations

So the overrides and finally checks happen way after the types are all worked out? OK. But ultimately that work has to happen, and it has to be checked even for an IDE to give early feedback as you type. So I’m not seeing how in practice inferring parameter types pulls in classes that wouldn’t be pulled in anyway. But I don’t maintain the compiler so I’ll take your word for it.

RIght, that’s why I said I’m not sure if it suffices to explain why.

I don’t even see how this could be, as the expected return type of the overridden method is used to type the overriding body:

trait A { def f: Int => Int }
object B extends A { def f = _ + 1 }

To clarify, I should have generalized my comment to talk about preferring to avoid inferring signatures for non-private members – whether it’s missing types for arguments or its result. For the result type, however, you have the RHS to validate the inferred type against, whereas with argument types you just have to take the inherited one. Also, what do you do for overloaded methods? Imagine you were inheriting the argument types from some method in a super class, and now someone adds an overload for that method – what should be the inferred signature in the subclass?

Regarding other checks happening later – yes, correct use of override/final is checked during refchecks, which is a phase after typers.

I’m not sure whether you’re using “validate” to mean “check” (as in the top-down process in bidirectional type inference, opposite to “infer” which goes bottom-up). But in any case, don’t you agree that if you leave the return type off, the inherited return type is needed in order to complete type inference of the RHS?

Now, I agree that public methods should probably have all their types specified. But there are plenty of cases where you’re not implementing public methods, especially when creating anonymous class instances, which can be very common in some libraries/DSLs.

I don’t think anyone should expect that adding an overload to a base class will not break any code using that base class. Besides the brittle base class problem (what if the subclass was already defining the overload?), it may obviously break plain user code by making it ambiguous.

1 Like

Thanks @adriaanm - that’s clearer to me now. Sorry, I don’t have the guts of how the compiler works baked into my brain yet. Perhaps one day.

The major case where this is an issue is in implementing anonymous, disposable instances. So you are typically implementing public methods, but you are not doing so in a context where a 3rd party can ever see a documentable implementation corresponding to that instance.

“whereas with argument types you just have to take the inherited one”

This is exactly the behaviour I want. No inference. No magic. Just take exactly the type you’d expect from the def being implemented.

“Also, what do you do for overloaded methods?”

Must override a def of

  • the same name
  • the same arity
    Must not
  • have zero candidates
  • have more than 1 candidates

“Imagine you were inheriting the argument types from some method in a super class, and now someone adds an overload for that method – what should be the inferred signature in the subclass?”

Damn those people who re-engineer superclasses that we extend! But in practice this happens now for normal defs. You only catch it on re-compile. And under my rules restricting this feature to things with exactly one candidates to override, it would then fail to compile, complaining that you’d attempted to not specify arg types on a def with multiple candidates for overriding.

I’ve refactored my code in some places: to use functions instead of anonymous classes
mostly due to lack of inferring arguments types.

//instead of 
new Maker[T] { 
  def applyTo(a:LongTypeName[AndGeneric,WithF[T]]) = ...
  def stateOf(a:LongTypeName[WithF[T],AndGeneric]) = ...
}

//i ended up with something like this which is shorter
new Maker[String](
  applyTO = { a => ... }, 
  stateOf = { b => ... }
)

//even if this could have better performence/readability:
new Maker[String] { 
  def applyTo(a) = ...
  def stateOf(a) = ...
}

If cost of this feature is too High then OK. Hopefully it could be reconsidered later (Scala 3.1) because as i see it does not break anything and all the stuff happens on compile time only.

1 Like

@adriaanm how about, in a future language version, requiring with a warning that all public signatures be given in full (including return types), but also allowing private signatures to be omitted (including argument types)?

“Private signatures” would include, as @drdozer says, what is not part of the API: the signatures of private members, and the signatures of private implementations of public methods (as in anonymous or private classes).

3 Likes

This has no sense at all. You cannot override privet methods by definition and this means that you cannot infer them at all. Inferring result is TOTALLY different than inferring argument types. Here you just take types from parent, that’s all. This type needs to be defined before (in parent) and it’ll be not manipulated in any way.

Problem with more than one candidate for that method is real but it can be solved in one or another way. We need just to chose safeness or convenience.

Sorry if I wasn’t clear, but it does make sense. I should have clarified that omitting argument types in private methods is obviously not possible. But if you paid more attention to what I wrote, you’d see that by “private signature” I do not only mean “the signatures of private members”. I also mean signatures that do not appear in public or protected APIs, which includes the signatures of public and protected overriding methods which appear in private and anonymous classes. For these, it makes sense to allow omitting parameter types!

Ok… This are places where this feature could be useful:

  • DSL’s
  • as anonymous implementation of Listeners/Callbacks that are not SAM’s
  • in scala.js facades

Most of them’ll use anonymous classes then your proposition could work. I still don’t get what’s wrong with inferring arguments for public methods? It always keeps parent type what is safe (much safer than inferring result type).

You can make it even tighter as:

Marker(applyTo = ..., stateOf = ...)

by supplying the appropriate apply.

trait Marker[T] {
  def applyTo(a: NastyTypeOf[T]): SomeType
  def stateOf(a: AnotherNasty[T]): AnotherType
}

Marker {
  def apply[T](applyTo: NastyTypeOf[T] => SomeType, 
                      stateOf:  AnotherNasty[T] => AnotherType) = {
    // need another name because there's no "function scope" equivalent of `this`
    val ato = applyTo
    val sof = stateOf
    new Maker[T] {
      override def applyTo(a: NastyTypeOf[T]) = ato(a)
      override def stateOf(a: AnotherNasty[T]) = sof(a)
    }
  }
}

This is all fairly boring mechanically generatable boilerplate. But it gives you inference, and normalised syntax.

Whole idea is not to make code tighter (option with new is tight enough).

You always can create helper methods but this is not equivalent to anonymous class:

  • it creates additional lambdas and this can lead to other problems.
  • With such implementation you NEED to implement both methods even if there is default implementation of one already.
  • What if you have 4 or more possible methods to override?
  • What if you need to use other methods that are available in body of Marker class?

Anonymous classes are very powerful tool (maybe too much even) that gives you abilities that are hard to replace by other language features.

I do agree with you. My point was more that since the language can support this through the mechanic I gave, it’s something that Scala can do. The issue is, from my pov, to provide syntax for this functionality (implementing traits/classes as anonymous classes) that doesn’t require hand-written boilerplate, and also minimises the code we write.

No, when there’s no overridden member to supply an inherited return type, we type check the RHS without an expected type and use that. The type we get from the overridden member is used as the expected type when we type check the RHS, so the inferred type could still be different.

You can see a few of the subtleties involved in the examples at scala/src/compiler/scala/tools/nsc/typechecker/Namers.scala at v2.12.8 · scala/scala · GitHub

Right, it just makes it a little bit worse again. If you had specified the types explicitly, it would be ok.