Функции высшего порядка над списками
Вернёмся ещё раз к задаче формирования списка из отрицательных чисел в исходном списке. На Котлине, данная задача допускает ещё и такое, очень короткое решение:
fun negativeList(list: List<Int>) = list.filter { it < 0 }
Это короткое решение, однако, является довольно ёмким в плане его содержания. Попробуем в нём разобраться.
list.filter — это один из примеров так называемой функции высшего порядка. Суть функции filter в том, что она фильтрует содержимое списка-получателя. Её результатом также является список, содержащий все элементы списка-получателя, удовлетворяющие определённому условию.
Как же она это делает и что такое вообще функция высшего порядка? Это тоже функция, которая, однако, принимает в качестве параметра другую функцию. Более подробная запись вызова filter выглядела бы так:
fun negativeList(list: List<Int>) = list.filter(fun(it: Int) = it < 0)
Функция-аргумент в данном случае должна иметь параметр it того же типа, что и элементы списка, и результат типа Boolean. В этой записи она отличается от обычной функции только отсутствием имени. Функция filter передаёт функции-аргументу каждый элемент списка. Если функция-аргумент вернула true, элемент помещается в список-результат, если false — он фильтруется.
Более короткая запись list.filter({ it < 0 }) использует так называемую лямбду { it < 0 } в качестве аргумента функции filter. Этот краткий синтаксис не включает в себя не только имени функции, но и ключевого слова fun, а также явного указания имён и типов параметров. При этом предполагается, что:
- параметр называется it; если параметру хочется дать другое имя, лямбда записывается как, например,
{ element -> element < 0 } - тип параметра — ровно тот, который требуется функции высшего порядка, для
filterэто тип элементов списка - тип результата — опять-таки ровно тот, что требуется
- в фигурные скобки помещается блок, определяющий результат функции; в идеале он состоит из одного оператора, в данном случае это
it < 0
Наконец, в том случае, если лямбда является последним аргументом функции, при вызове функции разрешается вынести её за круглые скобки: list.filter() { it < 0 }. Если других аргументов у функции нет, разрешается опустить в этой записи круглые скобки, получив запись из исходного примера: list.filter { it < 0 }
Функции высшего порядка с первого взгляда могут показаться очень сложными, но реально это довольно простая вещь, позволяющая свести запись алгоритмов к более компактной. Рассмотрим другую типичную задачу: из имеющегося массива целых чисел сформировать другой массив, содержащий квадраты чисел первого массива. Задача решается в одну строчку с помощью функции высшего порядка map:
fun squares(list: List<Int>) = list.map { it * it }
list.map предназначена для преобразования списка list в другой список такого же размера, при этом над каждым элементом списка list выполняется преобразование, указанное в функции-аргументе map. Тип параметра функции-аргумента совпадает с типом элементов списка list, а вот тип результата может быть произвольным. Например, преобразование list.map { "$it" } создаст из списка чисел вида [0, 1, 2] список строк [«0», «1», «2»].
Чуть более сложный пример: проверка числа на простоту.
fun isPrime(n: Int) = n >= 2 && (2..n/2).all { n % it != 0 }
Функция высшего порядка all в данном примере вызывается для получателя-интервала: 2..n/2. Применима она и для списка, как и для любого другого объекта, элементы которого можно перебрать с помощью for. Функция allимеет логический результат и возвращает true, если функция-аргумент возвращает true для ВСЕХ элементов списка. Тип параметра функции-аргумента совпадает с типом элементов списка, тип результата — Boolean. Аналогично можно было бы применить функцию высшего порядка any:
fun isNotPrime(n: Int) = n < 2 || (2..n/2).any { n % it == 0 }
Функция высшего порядка any возвращает true, если функция-аргумент возвращает true ХОТЯ БЫ для одного элемента списка.
Наконец, функция высшего порядка fold предназначена для «сворачивания» списка в один элемент или значение. Например:
fun multiplyAll(list: List<Int>) = list.fold(1.0) {
previousResult, element -> previousResult * element
}
Функция fold работает следующим образом. Изначально она берёт свой первый аргумент (в данном примере 1.0) и сохраняет его как текущий результат. Далее перебираются все элементы списка получателя и для каждого из них применяется указанная лямбда, которая из текущего результата previousResult с предыдущего шага и очередного элемента element делает текущий результат этого шага (в данном примере предыдущий результат домножается на очередной элемент). По окончании элементов списка последний текущий результат становится окончательным. В данном примере результатом будет произведение всех элементов списка (или 1.0, если список пуст).
Строки
Строки String во многих отношениях подобны спискам, хотя формально и не являются ими. Список состоит из однотипных элементов, к которым можно обращаться по индексу и перебирать с помощью цикла for. Строки же точно так же состоят из символов Char, к которым также можно обращаться по индексу и которые также можно перебирать с помощью цикла for.
Напомним, что строковый литерал (явно указанная строка) в Котлине записывается в двойных кавычках. Переменную name произвольного типа можно преобразовать в строку, используя запись "$name" — строковый шаблон, или чуть более сложную запись name.toString() с тем же самым результатом.
Как мы видим, $ внутри строкового литерала имеет специальный смысл — вместо $name в строку будет подставлено содержимое переменной name. Как быть, если мы хотим просто включить в строку символ доллара? В этом случае следует применить так называемое экранирование, добавив перед символом доллара символ \. Например: "The price is 9.99 \$".
Экранирование может применяться и для добавления в строку различных специальных символов, не имеющих своего обозначения либо имеющих специальный смысл внутри строкового литерала. Например: \n — символ новой строки, \t — символ табуляции, \\ — символ «обратная косая черта», \" — символ «двойная кавычка».
Строки в Котлине являются неизменяемыми, то есть изменить какой-либо символ в уже созданной строке нельзя, можно только создать новую. В этом смысле они аналогичны немутирующим спискам List.
Перечислим наиболее распространённые операции над строками:
string1 + string2— сложение или конкатенация строк, приписывание второй строки к первой.string + char— сложение строки и символа (с тем же смыслом).string.length— длина строки, то есть количество символов в ней.string.isEmpty(),string.isNotEmpty()— получение признаков пустоты и непустоты строки (Boolean).string[i]— индексация, то есть получение символа по целочисленному индексу (номеру)iв диапазоне от 0 доstring.length - 1.string.substring(from, to)— создание строки меньшего размера (подстроки), в который войдут символы строкиstringс индексамиfrom,from + 1, …,to - 2,to - 1. Символ с индексомtoне включается.char in string— проверка принадлежности символаcharстрокеstring.for (char in list) { … }— цикл for, перебирающий все символы строкиstring.string.first()— получение первого символа строки.string.last()— получение последнего символа строки.string.indexOf(char, startFrom)— найти индекс первого символаcharв строке, начиная с индексаstartFrom.string.lastIndexOf(char, startFrom)— найти индекс первого символаcharв строке, идя с конца и начиная с индексаstartFrom.string.toLowerCase()— преобразование строки в нижний регистр (то есть, замена прописных букв строчными).string.toUpperCase()— преобразование строки в верхний регистр (замена строчных букв прописными).string.capitalize()— замена первой буквы строки прописной.string.trim()— удаление из строки пробельных символов в начале и конце:" ab c "преобразуется в"ab c"
В качестве примера рассмотрим функцию, проверяющую, является ли строка палиндромом. В палиндроме первый символ должен быть равен последнему, второй предпоследнему и т.д. Пример палиндрома: «А роза упала на лапу Азора». Из этого примера видно, что одни и те же буквы в разном регистре следует считать равными с точки зрения данной задачи. Кроме этого, не следует принимать во внимание пробелы. Решение на Котлине может быть таким:
fun isPalindrome(str: String): Boolean {
val lowerCase = str.toLowerCase().filter { it != ' ' }
for (i in 0..lowerCase.length / 2) {
if (lowerCase[i] != lowerCase[lowerCase.length - i - 1]) return false
}
return true
}
Обратите внимание, что мы с самого начала переписываем исходную строку str в промежуточную переменную lowerCase, преобразуя все буквы в нижний регистр и удаляя из строки все пробелы. Функция filter работает для строк точно так же, как и для списков — в строке оставляются только те символы, для которых функция-аргумент { it != ' '} вернёт true. Затем мы перебираем символы в первой половине строки, сравнивая каждый из них с символом из второй половины. Символ с индексом 0 (первый) должен соответствовать символу с индексом length - 1(последнему) и так далее.
Примеры не жизненные какие-то. Для математиков, а не для программистов.
Прочел вашу фразу и вспомнил анекдот:
Урок в первом классе :
Учительница заносит и ставит на стол компьютер.
— Дети сколько компьютеров на столе?
— Один! — хором отвечают дети.
Учительница заносит и ставит на стол еще один компьютер
— Дети сколько компьютеров на столе?
— Два! — хором отвечают дети.
Идя за третьим компьютером учительница бурчит себе под нос :
— с яблоками было легче.