Rules for underscore usage in Higher Kinded Type parameters

Viewed 251

I wonder why the following works (note the Functor or Applicative are irrelevant for the example)

trait Functor[F[_]]
trait Applicative[F[_]] extends Functor[F]

But not

trait Functor[F[_]]
trait Applicative[F[_]] extends Functor[F[_]]

My issue is with F vs F[_]

What is the rule here ?

Especially the compiler gives a criptic error:

F[_] takes no type parameters, expected: one

3 Answers

Underscore (_) has different meaning at the definition site (1) and at a call site (2).

(1) At the definition site underscore means that a generic (type parameter) is a type constructor (* => *) rather than a proper type (*).

trait A[X]                        // X is a proper type, *
//     ^^^ definition
trait B[X]   extends  A[X]        // X is a proper type, *
//     ^^^ definition, ^^^ call

trait A[F[_]]                     // F is a type constructor, * => *
//      ^^^^ definition
trait B[F[_]]   extends  A[F]     // F is a type constructor, * => *
//      ^^^^ definition,  ^^^ call

Since at the definition site it's already emphasized once that F is a type constructor, at a call site it's always referred to as just F (no need in extra _ if you mean the same type constructor F).

(2) At a call site, for a type constructor (* => *) F, F[_] means an existential type. An existential type is a proper type (*).

trait A[X]                        // X is a proper type, *
//     ^^^ definition

trait B[F[_]]                     // F is a type constructor, * => *
                extends  A[F[_]]  // F[_] is an existential type, *
//      ^^^^ definition,   ^^^^ call

trait Functor[F[_]] means Functor requires a type constructor (a type with a hole that produces a new type when the hole is filled).

Then, here trait Applicative[F[_]] extends Functor[F] you are saying that Applicative also requires something with one hole. Thus, F is already understood to be something with one hole and so passing it to Functor makes sense.

Finally, here extends Functor[F[_]] is the same as extends Functor[F[T] forSome { type T } ] so you are filling the hole in F and thus it is not longer applicable to Functor.

There exists a plan in Scala 3.2 (Dotty) to make F[_] mean the same at both the definition site and call site

In Scala 3.2, the meaning of _ changes from wildcard to placeholder for type parameter.

so the following would become valid syntax

trait Applicative[F[_]] extends Functor[F[_]] // Both F[_] represent type lambda [X] =>> F[X]

The intention is to replace wildcard (existential) type F[_] with F[?]. This is already visible in Dotty REPL

Starting dotty REPL...
scala> val l: List[_] = List(42)
val l: List[?] = List(42)

and if compiled with source:3.1 -deprecation the warning is already raised

dotc -source:3.1 -deprecation Main.scala
-- Deprecation Warning: Main.scala:2:14 ----------------------------------------
2 |  val l: List[_] = List(42)
  |              ^
  |        `_` is deprecated for wildcard arguments of types: use `?` instead
1 warning found

For this purpose kind-projector has preemptively changed its type lambda syntax from F[?] to F[*] in order to free up ? for Scala 3 existential type.

As a side note consider how the kind of the type constructor can be expressed currently in Dotty

Starting dotty REPL...
scala> trait Functor[F <: [X] =>> Any]
     | trait Applicative[F <: [X] =>> Any] extends Functor[F]
// defined trait Functor
// defined trait Applicative

scala> trait Applicative[F <: [X] =>> Any] extends Functor[[X] =>> F[X]]
// defined trait Applicative

The kind [X] =>> Any is syntax proper akin to informal notation * => * mentioned by Dmytro above.

Related