Функции высшего порядка над списками
Вернёмся ещё раз к задаче формирования списка из отрицательных чисел в исходном списке. На Котлине, данная задача допускает ещё и такое, очень короткое решение:
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
(последнему) и так далее.
Примеры не жизненные какие-то. Для математиков, а не для программистов.
Прочел вашу фразу и вспомнил анекдот:
Урок в первом классе :
Учительница заносит и ставит на стол компьютер.
– Дети сколько компьютеров на столе?
– Один! – хором отвечают дети.
Учительница заносит и ставит на стол еще один компьютер
– Дети сколько компьютеров на столе?
– Два! – хором отвечают дети.
Идя за третьим компьютером учительница бурчит себе под нос :
– с яблоками было легче.