_
обозначает (в зависимости от контекста)
- конструктор типа - если он используется как в определении / ограничении параметра типа
def foo[F[_]]: Unit
- экзистенциальный тип - если применяется к чему-то который должен использоваться как правильный тип
def bar(f: F[_]): F[_]
Здесь мы хотим понять конструктор типа.
Конструктор типа будет (упрощая), что F
чего-то, что это что-то еще не определено, но мы можем применить к нему A
и сделать его F[A]
. Например,
List
может быть передано как F[_]
, потому что у него есть пробел, если мы заполним его, например, String
, он может стать List[String]
Option
также может быть передано как F[_]
, в нем есть пробел, если бы мы заполнили его, например, Int
, он стал бы Option[Int]
Double
нельзя использовать как F[_]
, потому что он не 't имеет пробел
Типы с "пробелом" часто обозначаются как * -> *
, а типы без них как *
. Мы могли бы читать *
просто как тип, а * -> *
как «тип, который принимает другой тип для формирования типа» - или как конструктор типа.
(Высшие типы, подобные только что упомянутому, сложны вещь сама по себе, так что было бы лучше, чтобы вы узнали о них больше, помимо этого вопроса).
*
(из плагин доброго проектора ) используется для доброй проекции - синтаксис основан на приведенной выше нотации, чтобы показать, где будет передаваться тип, если мы хотим создать новый тип:
Monad[F[List[*]]]
действительно похож на:
type UsefulAlias[A] = F[List[A]]
Monad[UsefulAlias]
за исключением того, что он работает без псевдонима типа.
Если бы это был Дотти, его можно было бы лучше выразить с помощью лямбда типа :
// Monad[F[List[*]]] is equal to
[A] =>> Monad[List[A]]
В вашем примере:
def make[F[_]: ContextShift: MonadError[*[_], Throwable]: Effect: Logging](): ...
F[_]
определяется как конструктор типа - поэтому вы не можете передать туда String
, Int
или Byte
, но вы можете передать туда List
, Future
или Option
( потому что они принимают один параметр типа) F[_]: ContextShift
- это ярлык для [F[_]](implicit sth: ContextShift[F])
- мы видим, что ContextShift
принимает в качестве параметра что-то, что принимает параметр типа сам по себе (например, F[_]
) [F[_]: MonadError[*[_], Throwable]
может быть расширено до: type Helper[G[_]] = MonadError[G, Throwable]
[F[_]: Helper]
, которое, в свою очередь, может быть переписано как type Helper[G[_]] = MonadError[G, Throwable]
[F[_]](implicit me: Helper[F])
или с использованием типа лямбда [F[_]] =>> MonadError[F, Throwable]
Вероятно, было бы легче читать, если бы оно было написано как:
def make[F[_]: ContextShift: MonadError[*, Throwable]: Effect: Logging]():
Дело в том, что *
предполагает, что ожидаемый тип -
[A] =>> MonadError[A, Throwable]
при этом доброта *
должна быть * -> *
вместо *
. Итак, это *[_]
означает «мы хотим создать здесь новый конструктор типа, сделав это вместо *
параметра, но мы хотим обозначить, что этот параметр имеет вид * -> *
вместо *
* 1101. *
[F[_]] =>> MonadError[F, Throwable]
, поэтому мы добавим [_]
, чтобы показать компилятору, что это конструктор типа.
Это довольно много для усвоения, и это должно быть проще, я могу только почувствовать извини и скажи что в Дотти будет понятнее.