ibanezn04
ibanezn04

Reputation: 518

Difference between * (star) and _ (underscore) in type parameter

Here someone says that star is underscore from scala 3, but I've seen some code like this in scala 2.13:

def make[F[_]: ContextShift: MonadError[*[_], Throwable]: Effect: Logging](): ...

Does it have a same meaning and just specify that type in * is not the same as in _?

Upvotes: 3

Views: 1552

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27535

_ denotes (depending on context)

  • type constructor - if used as in a type parameter definition/constraint
    def foo[F[_]]: Unit
    
  • existential type - if applied to something that should be used as a proper type
    def bar(f: F[_]): F[_]
    

Here we want to understand the type constructor.

Type constructor would be (simplifying) that F of something, that doesn't yet have that something defined, but we can apply A to it and make it a F[A]. E.g.

  • List could be passed as F[_] because it has a gap, if we fill it with e.g. String it could become List[String]
  • Option could be passed as F[_] as well, it has a gap, if we filled it with e.g. Int it would become Option[Int]
  • Double cannot be used as F[_], because it doesn't have a gap

Types with a "gap" are often denoted as * -> *, while types without them as *. We could read * simply as a type, while * -> * as "type that takes another type to form a type" - or a type constructor.

(Higher-kinded types like one just mentioned are complex thing on its own, so it would be better for you to learn about them more outside of that question).

* (from kind projector plugin) is used for kind projection - the syntax is inspired from the notation above to show where type would be passed if we wanted to create a new type:

Monad[F[List[*]]]

is really like:

type UsefulAlias[A] = F[List[A]]
Monad[UsefulAlias]

except that it works without a type alias.

If it was Dotty, it could be better expressed with a type lambda:

// Monad[F[List[*]]] is equal to
[A] =>> Monad[List[A]]

In your example:

def make[F[_]: ContextShift: MonadError[*[_], Throwable]: Effect: Logging](): ...
  • F[_] is defined as type constructor - so you cannot pass there String, Int or Byte, but you could pass there List, Future or Option (because they take one type parameter)
  • F[_]: ContextShift is a shortcut for [F[_]](implicit sth: ContextShift[F]) - we can see that ContextShift takes as a parameter something that takes a type parameter on its own (like F[_])
  • [F[_]: MonadError[*[_], Throwable] could be expanded to:
    type Helper[G[_]] = MonadError[G, Throwable]
    [F[_]: Helper]
    
    which in turn could be rewritten as
    type Helper[G[_]] = MonadError[G, Throwable]
    [F[_]](implicit me: Helper[F])
    
    or using a type lambda
    [F[_]] =>> MonadError[F, Throwable]
    

It would probably be easier to read if it was written as:

def make[F[_]: ContextShift: MonadError[*, Throwable]: Effect: Logging]():

Thing is, that * would suggest that expected type is

[A] =>> MonadError[A, Throwable]

meanwhile kindness of * should be * -> * instead of *. So this *[_] means "we want to create a new type constructor here by making this thing in place of * a parameter, but we want to denote that this parameter is of kind * -> * instead of *

[F[_]] =>> MonadError[F, Throwable]

so we'll add [_] to show the compiler that it is a type constructor.

It is quite a lot to absorb, and it should be easier, I can only feel sorry and say that in Dotty it will be clearer.

Upvotes: 12

Related Questions