В этой короткой статье я продемонстрирую ограничения обобщенного типа в 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.
Если вам понравился этот рассказ, мы рекомендуем прочитать наши Последние технические истории и Современные технические истории. До следующего раза не воспринимайте реалии мира как должное!