Разница между * (звездочка) и _ (подчеркивание) в параметре типа

Здесь кто-то говорит, что звездочка - это подчеркивание из scala 3, но я видел такой код в scala 2.13:

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

Имеет ли он такое же значение и просто указывает, что тип в * не такой же, как в _?

1 ответ

Решение

_ обозначает (в зависимости от контекста)

  • конструктор типа - если используется как в определении / ограничении параметра типа
    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[_], потому что в нем нет промежутка

Типы с "пробелом" часто обозначаются как * -> *, а типы без них как *. Мы могли читать* просто как тип, а * -> * как "тип, который принимает другой тип для формирования типа" - или конструктор типа.

(Высокородные типы, подобные только что упомянутому, сами по себе сложны, поэтому было бы лучше, если бы вы узнали о них больше, помимо этого вопроса).

*(от плагина доброго проектора) используется для проекции вида - синтаксис основан на обозначении выше, чтобы показать, куда будет передаваться тип, если мы хотим создать новый тип:

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]

Между тем доброта * должно быть * -> * вместо того *. Так это*[_] означает "мы хотим создать здесь конструктор нового типа, сделав это вместо * параметр, но мы хотим обозначить, что этот параметр имеет вид * -> * вместо того *

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

так что мы добавим [_] чтобы показать компилятору, что это конструктор типа.

Впитать довольно много, и должно быть легче, могу только пожалеть и сказать, что в Дотти будет понятнее.

Другие вопросы по тегам