Высокоуровневые функции и лямбды
В Kotlin функции являются функциями первого класса. Это значит, что они могут храниться в переменных и структурах данных, передаваться в качестве аргументов и возвращаться из других функций высшего порядка. Вы можете работать с функциями любым способом, который возможен для других нефункциональных значений.
Чтобы это облегчить, Kotlin, как статически типизированный язык программирования, использует семейство функциональных типов для представления функций и предоставляет набор специализированных языковых конструкций, таких как лямбда-выражения.
Функции высшего порядка
Функция высшего порядка — это функция, которая принимает функции как параметры, или возвращает функцию в качестве результата.
Хорошим примером такой функции является идиома функционального программирования fold для коллекций, которая принимает начальное значение — accumulator вместе с комбинирующей функцией и строит возвращаемое значение, последовательно комбинируя текущее значение accumulator с каждым элементом коллекции, заменяя значение accumulator .
R`, so it accepts a function that takes two arguments of types `R` and `T` and returns a value of type `R`. It is [invoked](#invoking-a-function-type-instance) inside the `for` loop, and the return value is then assigned to `accumulator`. —>
В приведённом выше коде параметр combine имеет функциональный тип (R, T) -> R , поэтому он принимает функцию, которая принимает два аргумента типа R и T и возвращает значение типа R . Он вызывается внутри цикла for и присваивает accumulator возвращаемое значение.
Чтобы вызвать fold , вы должны передать ему экземпляр функционального типа в качестве аргумента и лямбда-выражение (описание ниже). Лямбда-выражения часто используются в качестве параметра функции высшего порядка.
Функциональные типы
Kotlin использует семейство функциональных типов, таких как (Int) -> String , для объявлений, которые являются частью функций: val onClick: () -> Unit = . .
Эти типы имеют специальные обозначения, которые соответствуют сигнатурам функций, то есть их параметрам и возвращаемым значениям:
- У всех функциональных типов есть список с типами параметров, заключенный в скобки, и возвращаемый тип: (A, B) -> C обозначает тип, который предоставляет функции два принятых аргумента типа A и B , а также возвращает значение типа C . Список с типами параметров может быть пустым, как, например, в () -> A . Возвращаемый тип Unit не может быть опущен;
- У функциональных типов может быть дополнительный тип — получатель (ориг.: receiver), который указывается в объявлении перед точкой: тип A.(B) -> C описывает функции, которые могут быть вызваны для объекта-получателя A с параметром B и возвращаемым значением C . Литералы функций с объектом-приёмником часто используются вместе с этими типами;
-
(ориг.: suspending functions) принадлежат к особому виду функциональных типов, у которых в объявлении присутствует модификатор suspend , например, suspend () -> Unit или suspend A.(B) -> C .
Объявление функционального типа также может включать именованные параметры: (x: Int, y: Int) -> Point . Именованные параметры могут быть использованы для описания смысла каждого из параметров.
Чтобы указать, что функциональный тип может быть nullable, используйте круглые скобки: ((Int, Int) -> Int)? .
При помощи круглых скобок функциональные типы можно объединять: (Int) -> ((Int) -> Unit) .
The arrow notation is right-associative, `(Int) -> (Int) -> Unit` is equivalent to the previous example, but not to `((Int) -> (Int)) -> Unit`. —>
Стрелка в объявлении является правоассоциативной (ориг.: right-associative), т.е. объявление (Int) -> (Int) -> Unit эквивалентно объявлению из предыдущего примера, а не ((Int) -> (Int)) -> Unit .
Вы также можете присвоить функциональному типу альтернативное имя, используя псевдонимы типов.
Создание функционального типа
Существует несколько способов получить экземпляр функционального типа:
Используя блок с кодом внутри функционального литерала в одной из форм:
-
: < a, b -> a + b >, : fun(s: String): Int
Литералы функций с объектом-приёмником могут использоваться как значения функциональных типов с получателем.
Используя вызываемую ссылку на существующее объявление:
- функции верхнего уровня, локальной функции, функции-члена или функции-расширения: ::isOdd , String::toInt ,
- свойства верхнего уровня, члена или свойства-расширения: List<Int>::size , : ::Regex
К ним относятся привязанные вызываемые ссылки, которые указывают на член конкретного экземпляра: foo::toString .
- Используя экземпляр пользовательского класса, который реализует функциональный тип в качестве интерфейса:
При достаточной информации компилятор может самостоятельно вывести функциональный тип для переменной.
C` can be passed or assigned where a value of type `A.(B) -> C` is expected, and the other way around: —>
Небуквальные (ориг.: non-literal) значения функциональных типов с и без получателя являются взаимозаменяемыми, поэтому получатель может заменить первый параметр, и наоборот. Например, значение типа (A, B) -> C может быть передано или назначено там, где ожидается A.(B) -> C , и наоборот.
A function type with no receiver is inferred by default, even if a variable is initialized with a reference > to an extension function. > To alter that, specify the variable type explicitly. —>
Обратите внимание, что функциональный тип без получателя выводится по умолчанию, даже если переменная инициализируется со ссылкой на функцию-расширение. Чтобы это изменить, укажите тип переменной явно.
Вызов экземпляра функционального типа
Значение функционального типа может быть вызвано с помощью оператора invoke(. ) : f.invoke(x) или просто f(x) .
Если значение имеет тип получателя, то объект-приёмник должен быть передан в качестве первого аргумента. Другой способ вызвать значение функционального типа с получателем — это добавить его к объекту-приёмнику, как если бы это была функция-расширение: 1.foo(2) .
Встроенные функции
Иногда выгодно улучшить производительность функций высшего порядка, используя встроенные функции (ориг.: inline functions).
Лямбда-выражения и анонимные функции
Лямбда-выражения и анонимные функции — это «функциональный литерал», то есть необъявленная функция, которая немедленно используется в качестве выражения. Рассмотрим следующий пример:
Функция max является функцией высшего порядка, потому что она принимает функцию в качестве второго аргумента. Этот второй аргумент является выражением, которое в свою очередь есть функция, то есть функциональный литерал. Как функция он эквивалентен объявлению:
Синтаксис лямбда-выражений
Полная синтаксическая форма лямбда-выражений может быть представлена следующим образом:
- Лямбда-выражение всегда заключено в скобки <. >;
- Объявление параметров при таком синтаксисе происходит внутри этих скобок и может включать в себя аннотации типов;
- Тело функции начинается после знака -> ;
- Если тип возвращаемого значения не Unit , то в качестве возвращаемого типа принимается последнее (а возможно и единственное) выражение внутри тела лямбды.
Если вы вынесите все необязательные объявления, то, что останется, будет выглядеть следующим образом:
Передача лямбды в качестве последнего параметра
В Kotlin существует соглашение: если последний параметр функции является функцией, то лямбда-выражение, переданное в качестве соответствующего аргумента, может быть вынесено за круглые скобки.
Такой синтаксис также известен как trailing lambda.
Когда лямбда-выражение является единственным аргументом функции, круглые скобки могут быть опущены.
it: неявное имя единственного параметра
Очень часто лямбда-выражение имеет только один параметр.
` can be omitted. The parameter will be implicitly declared under the name `it`: —>
Если компилятор способен самостоятельно определить сигнатуру, то объявление параметра можно опустить вместе с -> . Параметр будет неявно объявлен под именем it .
Возвращение значения из лямбда-выражения
Вы можете вернуть значение из лямбды явно, используя оператор return. Либо неявно будет возвращено значение последнего выражения.
Таким образом, два следующих фрагмента равнозначны:
Это соглашение, вместе с передачей лямбда-выражения вне скобок, позволяет писать код в стиле LINQ.
Символ подчеркивания для неиспользуемых переменных
Если параметр лямбды не используется, то разрешено его имя заменить на символ подчёркивания.
Деструктуризация в лямбдах
Деструктуризация в лямбдах описана в Деструктурирующие объявления.
Анонимные функции
Единственной особенностью синтаксиса лямбда-выражений, о которой ещё не было сказано, является способность определять и назначать возвращаемый функцией тип. В большинстве случаев в этом нет особой необходимости, потому что он может быть вычислен автоматически. Однако, если у вас есть потребность в определении возвращаемого типа, вы можете воспользоваться альтернативным синтаксисом: анонимной функцией.
Объявление анонимной функции выглядит очень похоже на обычное объявление функции, за исключением того, что её имя опущено. Тело такой функции может быть описано и выражением (как показано выше), и блоком.
Параметры функции и возвращаемый тип обозначаются таким же образом, как в обычных функциях, за исключением того, что тип параметра может быть опущен, если его значение следует из контекста.
Аналогично и с типом возвращаемого значения: он вычисляется автоматически для функций-выражений или же должен быть явно определён (если не является типом Unit ) для анонимных функций с блоком в качестве тела.
When passing anonymous functions as parameters, place them inside the parentheses. The shorthand syntax that allows you to leave > the function outside the parentheses works only for lambda expressions. —>
Обратите внимание, что параметры анонимных функций всегда заключены в круглые скобки (. ) . Приём, позволяющий оставлять параметры вне скобок, работает только с лямбда-выражениями.
Одним из отличий лямбда-выражений от анонимных функций является поведение оператора return (non-local returns). Слово return , не имеющее метки ( @ ), всегда возвращается из функции, объявленной ключевым словом fun . Это означает, что return внутри лямбда-выражения возвратит выполнение к функции, включающей в себя это лямбда-выражение. Внутри анонимных функций оператор return , в свою очередь, выйдет, собственно, из анонимной функции.
Замыкания
Лямбда-выражение или анонимная функция (так же, как и локальная функция или анонимные объекты) имеет доступ к своему замыканию, то есть к переменным, объявленным вне этого выражения или функции. Переменные, захваченные в замыкании, могут быть изменены в лямбде.
Литералы функций с объектом-приёмником
Функциональные типы с получателем, такие как A.(B) -> C , могут быть вызваны с помощью особой формы – литералов функций с объектом-приёмником.
Как было сказано выше, Kotlin позволяет вызывать экземпляр функционального типа с получателем, предоставляющим объект-приёмник.
Внутри тела литерала объект-приёмник, переданный при вызове функции, становится неявным this , поэтому вы можете получить доступ к членам этого объекта-приёмника без каких-либо дополнительных определителей, а обращение к самому объекту-приёмнику осуществляется с помощью выражения this .
Это схоже с принципом работы функций-расширений, которые позволяют получить доступ к членам объекта-приёмника внутри тела функции.
Ниже приведён пример литерала с получателем вместе с его типом, где plus вызывается для объекта-приёмника:
Синтаксис анонимной функции позволяет вам явно указать тип приёмника. Это может быть полезно в случае, если вам нужно объявить переменную типа нашей функции для использования в дальнейшем.
Лямбда-выражения могут быть использованы как литералы функций с приёмником, когда тип приёмника может быть выведен из контекста. Один из самых важных примеров их использования это типобезопасные строители (ориг.: type-safe builders).
Kotlin передать функцию как параметр
Функции высокого порядка (high order function) — это функции, которые либо принимают функцию в качестве параметра, либо возвращают функцию, либо и то, и другое.
Функция как параметр функции
Чтобы функция могла принимать другую функцию через параметр, этот параметр должен представлять тип функции:
В данном случае функция displayMessage() через параметр mes принимает функцию типа () -> Unit , то есть такую функцию, которая не имеет параметров и ничего не возвращает.
При вызове этой функции мы можем передать этому параметру функцию, которая соответствует этому типу:
Рассмотрим пример параметра-функции, которая принимает параметры:
Здесь функция action принимает три параметра. Первые два параметра — значения типа Int. А третий параметр представляет функцию, которая имеет тип (Int, Int)-> Int , то есть принимает два числа и возвращает некоторое число.
В самой функции action вызываем эту параметр-функцию, передавая ей два числа, и полученный результат выводим на консоль.
При вызове функции action мы можем передать для ее третьего параметра конкретную функцию, которая соответствует этому параметру по типу:
Возвращение функции из функции
В более редких случаях может потребоваться возвратить функцию из другой функции. В этом случае для функции в качестве возвращаемого типа устанавливается тип другой функции. А в теле функции возвращается лямбда выражение. Например:
Здесь функция selectAction принимает один параметр — key, который представляет тип Int . В качестве возвращаемого типа у функции указан тип (Int, Int) -> Int . То есть selectAction будет возвращать некую функцию, которая принимает два параметра типа Int и возвращает объект типа Int.
В теле функции selectAction в зависимости от значения параметра key возвращается определенная функция, которая соответствует типу (Int, Int) -> Int .
Далее в функции main определяется переменная action1 хранит результат функции selectAction . Так как selectAction() возвращает функцию, то и переменная action1 будет хранить эту функцию. Затем через переменную action1 можно вызвать эту функцию.
Поскольку возвращаемая функция соответствует типу (Int, Int) -> Int , то при вызове в action1 необходимо передать два числа, и соответственно мы можем получить результат и вывести его на консоль.
Kotlin — Function as a parameter
It’s fun when you can pass the fun into another fun as a parameter. Ow, fun means function :p . Every function in kotlin is a fun. And it’s really fun!
Do you know what’s happing in this following code?
If you know then great! If not and you are interested about whats going on here then let’s explore more!
Recently I had do some works with a large codebase. For simplicity we were dividing the functions into smaller functions. Then somehow I faced the situation of calling different functions is going to need a bit more repeating code. Like, calling different functions in different confirmation dialog. Although the dialog is almost same but the functions I was calling after confirmation is a bit different. I came up with the idea of using a common interface with a common dialog for all. But, still it is a bit of code too and same code for every call. Then I went to the 2nd idea of passing the functions also. It was great and less of code. I did some searching and study also. Let’s share:
The simplest example:
This is the function we are going to pass as a parameter. The simplest one:
This is the function where we will pass our function:
And finally this is how we use it:
Not that hard huh! In the last function we passed the passMeAsParameter() function as a function parameter. See the second braces. They are necessary. But looks like a bit..um..not so hot. Here lambda comes in handy.
There is another cooler way to do this:
Yap. Just two colons(::) makes it more fun!
Okay let’s make it a bit harder. This time we will write a whole function as a parameter.
Simple, isn’t it! Now, how about passing a parameter in the parameter function!
A bit more complex version with more fun:
We don’t want to write same parameter twice. Let’s do this:
Yes, you are right. “it” means that “log” will go there. With lambda it will be more beautiful:
But we don’t want to write “it” too.
Again just two colons(::) makes it more fun!
Now let’s say, the function which we want to pass as parameter is in another class. No problem at all:
Want to pass more than one function? Okay
Here I used vararg. You can use arrays, list or anything you want.
Your function has a return type? No problem:
A more simpler (and b-e-a-uuuu-tiful) version of this code is:
That’s it! Now you know how to use function as a parameter. You will now also understand the code snipped we we were looking at the beginning of the tutorial. Kotlin is a very powerful language with many many awesome features. There are Coroutines, Android Extensions and many more! For now have fun with fun!
Функциональный Kotlin. Во имя добра, радуги и всего такого
Сам по себе Kotlin очень мощный инструмент, но многие часто используют его не на полную мощность, превращая его в какую-то. Java 6. Попробую рассказать почему так делать не надо и как использовать функциональные фичи языка на полную.
Функции высшего порядка
Начну с них. И, заодно, расскажу что это вообще такое: когда функция принимает другую функцию как параметр или возвращает ее — это функция высшего порядка. Просто, не правда ли? Но как этим пользоваться?
То, что функции в Котлине могут получать и возвращать другие функции для нас должно означать то, что мы можем записать их в переменную. Выглядеть это будет как-то так:
Тогда, мы сможем передать ее, используя синтаксис вида:
Отлично, но что если мы хотим использовать функцию, которую мы определили нормальным способом? Или вообще передать метод? Что-ж, для этого тоже есть возможность — ссылка на функцию. Вот так мы можем проверить строку на пустоту:
Перейдем к более конкретному кейсу. Допустим, нам нужно распарсить строки определенным образом, но только в одном месте в программе. Очевидно, что логика будет повторяться. Что делать? Создадим для этого отдельный объект с одним методом? Или пропишем для каждого «ручками» прямо на месте?
Нет, копить технический долг ни к чему, лучше мы сделаем, например, что-то такое:
Функции области видимости
Теперь, когда мы разобрались с ФВП, перейдем к вещам, которыми, скорее всего, пользовались все. let , run , with , apply , и also . Знакомые слова? Надеюсь, но все же разберем их.
Сначала let и also . Они наиболее просты и понятны, потому что все что они делают внутри — это вызов block(this) . По сути, мы просто делаем вызов определенной нами «на месте» функции. Разница лишь в том, что они возвращают. also используется когда вы хотите продолжить работу с тем же объектом, у которого решили вызвать функцию и let , если вам нужно вернуть новый объект.
Теперь к run , with и apply :
run очень похож на let , apply на also , а with это почти то же самое что и run , просто receiver получаем разными способами. Чем они удобны и зачем нужны, если есть let и also ? Все просто, тут вместо it используется this , который вообще можно опустить и не писать.
И как же эффективно использовать все эти функции? Мы можем и в большинстве случаев даже должны писать все наши функции полностью в функциях областей видимости.
Так мы получим гарантии, что наши промежуточные значения живут ровно столько, сколько нужно, а код становится проще читать, поскольку все преобразования исходных данных теперь вытекают друг из друга, позволяя нам иметь в каждой подзадаче ровно столько информации, сколько нам надо для ее решения. Функциональная «изоляция» позволит в том числе упростить чтение и тестирование своего кода.
Вы же заметили слово inline в объявлении? А оно, кстати, очень важно. К сожалению, использование функций высшего порядка влечёт за собой снижение производительности, так как функция является объектом и происходит захват контекста замыканием, то есть функции становятся доступны переменные, объявленные вне её тела.
Разумное применение встроенных функций позволяет решить эту проблему. Но делать блоки встроенных функций слишком большими все равно не стоит.
Классы и Объекты
Все мы уже должны были понять, что плодить иерархии и создавать экземпляры классов там, где никакого специфического поведения у отдельных экземпляров нет — дурацкая идея (хотя есть и исключения)?
Это лишний раз усложняет структуру и мешает нам писать код из цепочек функций, потому что нам нужно сначала создать бесполезный экземпляр, и только потом мы сможем воспользоваться его функционалом в блоке области видимости:
Использование объекта позволило бы нам сделать наш код проще:
Как хорошо, когда нет ничего лишнего, да?
Но допустим мы все же вынуждены иметь дело с классом, у которого есть поведение, но экземпляр класса создавать не хотим? К счастью, и для этого найдется достаточно простое решение. Мы просто скинем всю статику, которая есть в объекте в companion object :
Теперь мы можем сделать так же, как и с объектом.
Factory методы
Для начала я приведу пример кода:
Лично мне он не нравится. Логическая цепочка тут нарушена, поскольку мы в первую очередь хотим понимать что создание экземпляра класса Other и вызов его метода нужны просто чтобы создать экземпляр класса Some, но запись не дает моментально этого понять.
Конечно, мы можем заинлайнить его:
Но у меня есть вариант получше. Было бы неплохо, если бы мы могли привязать логику, нужную только для создания объекта к. созданию объекта? И мы можем, для этого нам всего лишь нужно написать вот такой Factory-метод:
Теперь мы можем сделать так:
Или если класс Other тоже имеет свой фабричный метод:
Понимаю, на первый взгляд вообще стало хуже. но что если функций и необходимых объектов в иерархии станет сильно больше? Иерархия может быть достаточно большой, а данные могут требовать много преобразований. Не пытайтесь использовать это всегда и везде, но задумайтесь об этом на стадии рефакторинга.
Подводные камни функций-расширений
Предполагаю, что не знать о наличии в Котлине возможности писать функции-расширения и как это делать могут совсем новички, но на всякий случай я объясню. Это функция, с которой вы можете взаимодействовать так же, как с методом класса. Вот как это выглядит:
Или если хотите экзотики:
Первое, что стоит знать о таких функциях — это их приоритет исполнения. Вы не сможете перекрыть метод класса функцией-расширением. Хотя если вы напишете функцию вторым способом, IntelliJ IDEA будет показывать, как будто вы используете именно расширение — не обманывайтесь, я проверял.
Теперь, давайте поговорим о ссылках на функции-расширения. Если мы сделали ее через val то все просто и понятно, передаем ее просто как foo и ничего лишнего, работаем как с любой другой переменной. Но если через fun , появляются нюансы, на которые имеет смысл обратить внимание.
Давайте посмотрим на вот такой код:
В принципе, ничто не мешает нам так сделать, но есть одно «но», из-за которого я считаю это очень плохой практикой.
Все дело в том, что передать ссылку на эту функцию просто не получится. Никак. Даже внутри самого класса. Если так сделать, то Котлин просто не поймет, как обращаться к этой функции — как к методу класса Some или Other.
Однако, если мы, например, вынесем расширения в отдельный файлик под расширения определённого класса или вообще растащим их по пакетам на свое усмотрение — сможем легко обратиться к функции как Some::someExtention . Естественно, в данном случае класс или объект не важно — поведение будет одинаковым.
Кстати, если вы немножко копались в устройстве Котлина, то вот вам еще немного бесполезной информации. Допустим, вы решили непонятно зачем использовать вместо нормального способа передачи функции как параметра класс KFunction.
В этом случае у меня две новости — первая в том, что ваши вкусы весьма специфичны, а вторая, что в этом случае наша функция-расширение обрабатывается в качестве самой обычной функции, у которой первый параметр это экземпляр класса, который она расширяет.