Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the parameter type of a generic function not inferred?

Tags:

generics

scala

In Scala 2.11.7, having the following case class and an additional apply method:

case class FieldValidator[T](key: String, isValid: T => Boolean, 
                             errorMessage: Option[String] = None)

object FieldValidator {
  def apply[T](key: String, isValid: T => Boolean, 
               errorMessage: String): FieldValidator[T] = ???
}

when I try to use:

FieldValidator[String](key, v => !required || v.nonEmpty, "xxx")

I'm getting a "missing parameter type" compilation error pointing at v.

When I explicitly specify the type of v, it compiles fine, and I can even skip the generic type of the apply method, i.e.

FieldValidator(key, (v: String) => !required || v.nonEmpty, "xxx")

Why isn't the type of v inferred when just the generic type of apply is provided?

like image 931
Jacek Kunicki Avatar asked Jan 24 '19 13:01

Jacek Kunicki


People also ask

What is a generic type parameter?

Generic Methods A type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.

Can you declare a variable with a generic type?

Notice that the type variables declared by a generic type can be used only by the instance fields and methods (and nested types) of the type and not by static fields and methods. The reason, of course, is that it is instances of generic types that are parameterized.

How do you define generic function?

A generic function has a set of bodies of code of which a subset is selected for execution. The selected bodies of code and the manner of their combination are determined by the classes or identities of one or more of the arguments to the generic function and by its method combination type.


1 Answers

It's not so much about generics, it's rather a problem with overloading and default parameters.

First, recall that since FieldValidator is a case-class, a synthetic factory method

def apply(
  key: String,
  isValid: T => Boolean, 
  errorMessage: Option[String] = None
)

is automatically added to the companion object FieldValidator. This results in Field validator having two generic methods with default parameters and same name.

Here is a shorter example that behaves in roughly the same way:

def foo[A](f: A => Boolean, x: Int = 0): Unit =  {}
def foo[A](f: A => Boolean, x: String): Unit =  {}

foo[String](_.isEmpty)

It results in:

error: missing parameter type for expanded function ((x$1: ) => x$1.isEmpty)

 foo[String](_.isEmpty)
             ^

I can't pinpoint what exactly goes wrong, but essentially, you have confused the compiler with too much ambiguity by throwing three different sorts of polymorphism at it:

  • Overloading: you have two methods with name apply
  • Generics: your methods have a generic type parameter [A]
  • Default arguments: your errorMessage (x in my shorter example) can be omitted.

Together, this leaves the compiler with the choice between two equally named methods with unclear types and unclear number of expected type arguments. While flexibility is good, too much flexibility is simply too much, and the compiler gives up trying to figure out what you wanted, and forces you to specify all types of every single argument explicitly, without relying on inference.

Theoretically, it could have figured it out in this particular case, but this would require much more complex inference algorithms and much more backtracking and trial-and-error (which would slow down the compilation in the general case). You don't want the compiler to spend half a day playing typesystem-sudoku, even if it theoretically could figure out a unique solution. Exiting quickly with an error message is a reasonable alternative.


Workaround

As a simple work-around, consider reordering the arguments in a way that allows the compiler to eliminate ambiguity as fast as possible. For example, splitting the arguments into two argument lists, with two Strings coming first, would make it unambiguous:

case class FieldValidator[T](
  key: String,
  isValid: T => Boolean, 
  errorMessage: Option[String] = None
)

object FieldValidator {
  def apply[T]
    (key: String, errorMessage: String)
    (isValid: T => Boolean)
  : FieldValidator[T] = {
    ???
  }
}

val f = FieldValidator[String]("key", "err"){
  s => s.nonEmpty
}
like image 127
Andrey Tyukin Avatar answered Sep 27 '22 17:09

Andrey Tyukin