Search code examples
scalafunctional-programmingscala-catskind-projector

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 _?


Solution

  • _ 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.