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