В этой короткой статье я продемонстрирую ограничения обобщенного типа в Scala - что это такое, зачем они нужны и как они используются.

Для начала позвольте мне показать вам две головоломки Scala.

Головоломка номер один:

def pair[S](s1: S, s2: S): (S, S) = (s1, s2)

Как вы думаете, что произойдет, если мы передадим в этот метод два значения разного типа? Давайте посмотрим:

scala> pair(“foo”, 1)
res0: (Any, Any) = (foo,1)

Произошло то, что компилятор сказал: «Хорошо, первое значение - это String, а второе значение - это Int, но на самом деле это не сработает, потому что мне нужно, чтобы эти два аргумента были одного типа. Сейчас я попытаюсь найти их ближайший общий супертип. Вот оно - Любое. Круто, каждый из этих аргументов также является Any, поэтому я буду считать, что они принадлежат к типу Any, чтобы удовлетворить ограничению типа ».

Кстати, если бы у нас была привязка к типу S, например:

def pairVals[S <: AnyVal](s1: S, s2: S) = (s1, s2)

Затем вызов метода с String и Int приведет к ошибке компиляции, потому что один из аргументов не является подтипом AnyVal (помните, что String является подтипом AnyRef, а не AnyVal). Компилятор говорит: «Ближайший общий супертип аргументов, которые вы мне дали, - это Any, но это нарушает данное ограничение типа»:

scala> pairVals(“foo”, 4)
<console>:12: error: inferred type arguments [Any] do not conform to method pairVals’s type parameter bounds [S <: AnyVal]

Вернуться к методу pair (). Возникает вопрос: что, если мы хотим сказать, что «метод pair () должен принимать два аргумента одного типа»? Мы видели, что pair () легко позволяет нам передавать String и Int; компилятор просто выведет, что вы передали значения типа Any. Что, если мы хотим разрешить только вызовы, в которых оба аргумента имеют один и тот же тип (например, два Ints или две строки, но не один Int и один String)?

Прежде чем я отвечу на этот вопрос, вот еще одна загадка:

Головоломка номер два:

def advPair[S <: T, T](s: S, t: T): (S, T) = (s, t)

Это тот же принцип, но немного в другой ситуации. Как вы думаете, что происходит, когда мы вызываем этот метод с двумя разными аргументами?

scala> advPair(“foo”, 1)
res0: (String, Any) = (foo,1)

На первый взгляд вы можете заключить, что advPair () принимает два аргумента, первый из которых является подтипом второго, и по этой причине вызов его с помощью String и Int завершится ошибкой (поскольку String не является подтипом Int). Опять же, вы ошибаетесь. Как и в первом случае, компилятор попытается устранить беспорядок и посмотреть, сможет ли он удовлетворить ограничения типа, повысив ваши значения. (Кстати, не возражайте, если я использую термин «апкастинг» - я знаю, что это звучит немного некрасиво, но мы с вами оба знаем, что ничего уродливого не происходит. Int * - это * Any, как и все остальное в Scala).

Итак, да, мы передали String и Int, но Int также Any. Когда мы смотрим на вещи с этой точки зрения, все работает - мы передали String и Any, и условие первого аргумента, являющегося подтипом второго, выполнено.

Итак, как мы можем наложить ограничения на «исходные» типы без того, чтобы компилятор старался проявлять щедрость и повышал качество наших аргументов до тех пор, пока ограничения типа не будут удовлетворены?

Ответ (ы):

Используя ограничения обобщенного типа.

Решена первая загадка:

def pair[S, T](s: S, t: T)(implicit ev: S =:= T): (S, T) = (s, t)

Вторая загадка решена:

def advPair[S, T](s: S, t: T)(implicit ev: S <:< T):(S, T) = (s, t)

Видите эти неявные параметры? Они - выход из нашего затруднительного положения. Вы можете рассматривать их как типы: оператор S =: = T имеет ту же природу, что, например, Map [S, T]. Просто ограничение обобщенного типа (GTC) является инфиксным, а не префиксом.

Так что же происходит, когда у вас есть GTC? Что ж, компилятор совершит магию вывода, как если бы GTC не было. Как только все будет решено, он проверит, удовлетворен ли GTC. Посмотрите, как в примере pair () у нас больше нет двух аргументов типа S, но аргументы разных типов, а также изменяется тип возвращаемого значения. из (S, S) в (S, T)? Таким образом, компилятору не нужно будет выполнять какие-либо действия по повышению качества - он просто объединит заданные аргументы в кортеж. Однако перед тем, как продолжить работу с телом метода, он проверит соответствие GTC; то есть, действительно ли типы S и T являются одним и тем же типом.

scala> pair(“foo”, 1)
<console>:12: error: Cannot prove that String =:= Int.
 pair(“w”, 1)
 ^
scala> pair(“foo”, “foo”)
res0: (String, String) = (w,a)

Второй метод также претерпел некоторые изменения в ограничениях типов - мы удалили часть [S ‹: T]. Почему? Опять же, чтобы компилятор не совершал никаких магических действий. Мы видели, как наличие [S ‹: T] заставляло компилятор преобразовывать Int в Any, чтобы соответствовать ограничению типа, верно? Что ж, теперь компилятор с радостью поймет, что ограничений типа нет, что означает, что S и T могут быть String и Int без каких-либо проблем, поэтому он объявит свое окончательное решение: S - String, T - Int. Но затем вмешивается GTC и говорит компилятору: «Хорошо, теперь, когда вы разрешили S и T для конкретных типов, вот мои требования». И если S не является подтипом T, вызов завершится ошибкой.

scala> advPair(“foo”, 1)
<console>:12: error: Cannot prove that String <:< Int.
 advPair(“foo”, 1)
 ^
scala> val any: Any = "any"
any: Any = any
scala> advPair("foo", any)
res0: (String, Any) = (foo,any)

Заключение

Итак, вот краткое изложение:

Компилятор будет пытаться соответствовать ограничениям типов, повышая типы по мере необходимости, отчаянно пытаясь найти комбинацию, которая работает. Если процесс не удастся, компилятор заплачет. В случае успеха компилятор присвоит каждому параметру универсального типа конкретный тип (например, S и T станут String и Int).

Затем, когда типы разрешены, компилятор увидит, что требуется неявный GTC, и предоставит его. Обратите внимание: когда вы пишете метод, которому требуется GTC, вам просто нужно объявить его как неявный параметр, и все. Компилятор предоставит необходимый GTC во время компиляции и подключит разрешенные типы, выдав сообщение об ошибке в случае, если GTC не удовлетворен.

В Scala развлечения с типами никогда не заканчиваются. :) Как обычно, напишите мне на [email protected] с обратной связью или найдите меня в Твиттере.

Хакерский полдень - это то, с чего хакеры начинают свои дни. Мы часть семьи @AMI. Сейчас мы принимаем заявки и рады обсуждать рекламные и спонсорские возможности.

Чтобы узнать больше, прочтите нашу страницу о нас, поставьте лайк / напишите нам в Facebook или просто tweet / DM @HackerNoon.

Если вам понравился этот рассказ, мы рекомендуем прочитать наши Последние технические истории и Современные технические истории. До следующего раза не воспринимайте реалии мира как должное!