Even less explicit typing on def implementations


#1

Can we reduce clutter by allowing implementations of defs to elide parameter types?

trait Foo {
  def doFoo(age: Int, name: String): List[Double]
}

val myFoo = new Foo {
  override def doFoo(age, name) = age.toDouble :: name.length.toDouble :: Null
}

We already in effect allow this for the return type. The rules would be:

  1. the type must be declared in the original declaration
  2. types can only be elided for parameters in a def that overrides an earlier declaration
  3. the inferred types for a parameter without an explicit type declaration is taken to be identical to that of the type in the abstract declaration that it is overriding.

This is a minor annoyance for simple types, but once you have type lambdas and other magics going on, explicitly writing out the argument type(s), and in some cases, even working out what the would be, is complicated and clutters up the source code.


#2

I’m not wanting to be antagonistic, but I really don’t like this idea. It seems a high amount of language complication for little gain.


#3

Omitting the return type of a public method is legal, but considered bad style. For argument types probably more so.

It would not be straight-forward to find the ancestor that defines the argument type.


#4

?!

I’m not sure whether I like the proposal, but the conceptual equivalent of

m.methods.filter(_.name == myName && _.nargs == myArgN) match {
  case found :: Nil => Right(found)
  case Nil => Left("Error: method overrides nothing")
  case lots => Left("Ambiguous override: ${lots.mkString(", ")} all match")
}

would do it.


#5

Exactly. Search on name. Must override something of the same name. Must have the same number of arguments. Must be the only one eligible for overriding with that number of methods.


#6

This isn’t intended as good style for implementations that are visible. It’s intended for all those anonymous implementations that are not named and can’t be instantiated outside of where they are declared.


#7

You can have an extra type members to remove duplication, eg:

trait MyTrait {
  type T1 = Future[Option[Either[List[String], Map[Int, Long]]]]

  type T2[Elem] = Try[Vector[Either[String, Option[Elem]]]]

  def method[Elem](param1: T1, param2: T2[Elem]): Unit
}

class MyClass extends MyTrait {
  override def method[Elem](param1: T1, param2: T2[Elem]): Unit = ???
}

I see such shortcuts from time to time.

In case of SAM types you can use function syntax to implement them, which allows to omit parameter types together with return type.


#8
sbt:foo> ++2.12.8!
sbt:foo> set scalacOptions += "-Yinfer-argument-types"
sbt:foo> consoleQuick
[info] Starting scala interpreter...
Welcome to Scala 2.12.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_152).
Type in expressions for evaluation. Or try :help.

scala> trait Foo {
     |   def doFoo(age: Int, name: String): List[Double]
     | }
defined trait Foo

scala> val myFoo = new Foo {
     |   def doFoo(age, name) = age.toDouble :: name.length.toDouble :: Nil
     | }
myFoo: Foo = $anon$1@36ca6b9

See https://github.com/scala/scala/pull/6505


#9

In case of SAM types you can use function syntax to implement them, which allows to omit parameter types together with return type.

So this is the point, isn’t it. The compiler is clearly capable of doing this inference because it will adapt the SAM to a function expression. Buf this isn’t possible for callbacks with, say, a success and failure method.

type T1 = Future[Option[Either[List[String], Map[Int, Long]]]]

Yeah, this is exactly what I want to avoid because a) it’s me doing the compiler’s job for it, and b) at no point do we end up with readable code. We now have to use T1 and T2 in the method declaration, which is yet another thing that the next person reading the code needs to read and then chase down.

To reiterate, it’s at the point of overriding abstract methods in throw-away implementations that I want to be able to skip providing types.


#10

Cheers. I didn’t know that exists in 2.12.8, but wouldn’t it be great if it was baked into scala 3 <3


#11

That -Yinfer-argument-types option has been removed in 2.13. We don’t intend to bring it back, sorry. Multiple reasons: compiler performance and complexity, also the potential for abuse: 99% of the time you shouldn’t have those inferred.


#12

The thing is, in order to have them inferrred, I just end up mechanically converting:

def something(something: A, somethingElse: B): C

into:

def something: (A, B) => C

Just to get type inference at the throw-away implementation sites. But now we’ve lost the opportunity for stylized and enforceable documentation in the original declaration. And at the potential run-time cost of allocating that function instance.


#13

There wasn’t much positive feedback for this, and I’ve thought about it some more and come up with something more general that handles this and other things: Allow defs to be implemented from functions


#14

I’m sorry, but that proposal is much worse. It’s a half-step towards erasing the distinction between functions and methods, making it impossible to ensure what you mean while still making people care about which it is.

This one is far better. The rules are simple and clear, and the infrastructure mostly exists to do it; the only question is whether it’s a good idea.

This is not the same thing as -Yinfer-argument-types. There’s no type inference going on here, just looking it up in the parent. (That’s done anyway with explicit types to figure out whether you need to override.)

Okay, yes it is. Oops.

So, plus is: less typing and less keeping track of potentially big and awkward types; the minus is: less explcit, and another language feature to keep track of.


#15

I thought that that is what Yinfer-argument-types actually does, despite its name.


#16

Yeah, oops, you’re right.

I really don’t understand @adriaanm’s comment then.

I really don’t get the “potential for abuse” part. This forces the implementer to use the required types. The compiler yells at you now anyway if you don’t exactly copy the type signature. There’s no flexibility; the only “abuse” is that you don’t need to repeat the types when they’re completely defined anyway. I don’t see how it’s any more of an abuse than closures.

The only tiny difference (benefit!) is that if you’re confused about which trait implementations have defaults and which don’t, and you also muck up the types, inferring the types will catch your (double) mistake. I’ve done this before; thought I had to supply method foo with types P and Q, but actually foo was provided and used types P and R, and so my code compiled but failed. Inferring argument types fixes that.

I can’t easily tell how it would affect compiler performance, but the relevant code doesn’t seem extensive, and the return type is already inferred so the wildcarding of types already has to happen. And the equivalent computation already has to be performed for the corresponding method that returns a function.

So I can’t really see how these could be the most compelling reasons not to have the (already implemented!) feature. Maybe there are implementation details that actually make it muck things up a lot more than it seems. But on the surface, it seems that there must be other reasons, and these factors are just (small) fringe benefits?


#17

:(.

I’ve never heard about this feature. To be honest for me it also seams to be good idea. With inferring result type we could screw up public api. Arguments seams to be much safer and can not imagine how it could hurt us.


#18

Having to compute the overrides / overloads of a member while computing its signature is potentially very expensive (all base types need to be considered, potentially causing a long chain of class loading), and it creates more coupling between different compilation units (which is also bad purely from a SW eng perspective). This reduces the potential for incremental compilation / parallel type checking (in future / in hydra). To improve type checking, we must make type checking (and name + signature resolution) a more local operation than it is now.


#19

So, under this proposal, if you don’t supply types for the arguments of a def, the compiler goes up the hierarchy until it finds a method with the same name and arity, and use those types. If it’s ambiguous or none matching can be found, it barfs. Do we not have to essentially do this work already for every def marked overrides? We need to validate that it does, in fact, override something. And there’s the edge case where the overriding declaration potentially widens the argument types that the current explicitly typed arguments needs to handle. Lastly, we have to do this walking on all the other defs, to check that they aren’t overriding a final declaration.


#20

I think that happens at a later phase. I’m not sure if that explains it.