Данная статья является текстовым представлением вот этого видео
Прежде чем мы начнём говорить про inline функции, необходимо провезти ликбез по функциям высшего порядка, для чего неплохо было бы понять концепцию функционального программирования, потому как все эти понятия связаны между собой.
Меня очень часто спрашивают во всех соц. сетях нужна ли программисту математика. Я уже отвечал в одном из видео, что математика тренирует вашу мозговую мышцу, если так можно выразиться. Но помимо этого важно помнить, что большинство концепций (именно фундаментальных концепций) перекочевали в программирование именно из математики. В общем-то и не удивительно, ведь программируя мы фактически управляем цифрами и дальше всё более и более сложными концепциями, которые надстраиваются над ними.
Например, возьмем все то же функциональное программирование. Я уверен, вы уже много где слышали споры вроде "а является ли Котлин полноценным ФП языком?" или "на смену парадигме ООП приходит парадигма ФП". А ведь концепция функционального программирования появилась аж в 1958 году! Вдумайтесь. Прошло почти 70 лет, а мы до сих пор приходим в языках к этой концепции в попсовом, массовом программировании.
Каждая концепция привносит ограничения. И на самом деле ограничения это невероятно мощный инструмент. Там, где существуют ограничения - существует порядок. Конечно, всегда можно перегнуть палку и удариться в ограничения ради ограничений. Но, как мы понимаем, в концепциях программирования чаще всего ограничения разумны.
В чем же основная идея функционального программирования? В понятии математической функции. Вспомните из школы стандартный наш пример y = x^2. Мы точно знаем значения функции в зависимости от аргумента. Передадим 2 будет 4, передадим 10, будет 100 и так далее. В чем тут отличие от концепции ООП? В том, что в случае с ООП у нас есть поля класса, которые мы легко можем использовать в функции внутри языка программирования.
То есть сколько раз вы делали функцию из разряда: возьмем параметр и на основании его и какого-нибудь поля в классе выдадим результат? Думаю не совру, если скажу, что вы делаете такое постоянно.
Так вот, основное отличие ФП в том, что в функцию передается ровно то, что нужно функции и ничего больше. И значение вычисляется только на основании этих параметров и ничего больше. Таким образом мы автоматически получаем целый ряд преимуществ. Например, чистота функции гарантирует нам, что если вы покроете весь ваш код юнит-тестами, то скорее всего он будет работать правильно. Также вам легче переиспользовать одинаковые куски кода, ведь тогда у вас получается такой как бы модуль сам в себе, всё что нужно описано на входе и всё что выйдет — на выходе. Никаких сюрпризов внутри.
Вообще говоря про Singleton, по-моему его излишне демонизируют, так как основная его проблема в том, что если он начинает менять состояние внутри себя, то у вас просто нет возможности воспроизвести четкий путь к проблеме внутри теста. В целом вся функциональщина лишена этого недостатка.
Разумеется, вы не найдете абсолютно ни одного примера андроид приложения сделанного чисто на ФП просто потому, что мы зависимы от фреймворка Андроид, а он сделан в парадигме ООП. Нам еще очень долго придется жить с этим, однако, элементы ФП вместе с котлином всё больше и больше проникают в приложения, и здесь нетерпеливый зритель может восклинкнуть: "Алексей, видео же про inline function и классы, вы точно названием не ошиблись?" Терпение мой друг, скоро дойдем и до них.
Дело в том, что в математики существуют не только функции, но и так называемые функции высшего порядка - это функции, которые принимают в качестве аргументов другие функции. Например y = f(z, f(x)), ну или чтоб было понятнее то, что вы видите на изображении.
То есть у вас есть функция, которая что-то делает с двумя стрингами и вы хотите к этому что-то добавлять, какой-нибудь текст. Но вам нужно прям внутри вызова функции определять что конкретно вы хотите сделать с этими двумя стрингами. Иногда надо конкатенировать, иногда надо вычитать общие символы и так далее. Вот в этом случае как раз вам и пригодятся функции высшего порядка.
Понятно, что пример очень простой, я бы даже сказал примитивный, но думаю понятно как работает. Примеров использования вы сможете найти вагон и маленькую тележку в исходниках котлина того же, да и в крупных проектах хватает.
Но у функций высшего порядка есть один минус — они дорогие. Не в плане, что с вас за сеанс кодинга в конце возьмут 50000 рублей, а для ресурсов вашего устройства. Мы помним, что в руках у нас не суперкомпьютер, а маленький и часто слабенький телефон, поэтому ресурсы для имеют критически важное значение.
Давайте рассмотрим пример с данным кодом.
Сразу скажу, в машинах я разбираюсь так себе, потому у меня феррари с дизельным двигателем. Я даже не знаю нормально это или нет. Но для примера должно быть понятно. Вот у нас конкретный заводик по производству автомобилей, он зависит от поставок двигателей и от отдельной сборочной, куда поставляются эти двигатели и какое-то количество рабочих, а она уже возвращает машины. Соответственно, где-то мы можем вот такую как на рисунке сборочную линию сделать, а в другом месте мы сможем развернуть уже другой подход.
Вот у нас здесь классическая функция высшего порядка, которая принимает в себя не только двигатели, но и отдельную сборочную линию, которая сама по себе является функцией, которая возвращает нам машины. И после компиляции мы получим нечто вроде кода ниже.
Не удивляйтесь вызову чего-то у null, в Java так можно делать, если инстанс статический. Да, такая вот магия Java. Что мы собственно здесь видим: наша лямбда, то есть встроенная функция в функцию превратилась в обычный интерфейс , а дальше всё очень просто.
Создается отдельный класс, внутри которого мы будем иметь одну простую функцию под названием invoke, которая и вызывается. Никакой магии, просто ловкость рук и наша лямбда превращается в стандартный класс. Это теоретически ведет к увеличению расходов по памяти и так далее.
И вот здесь нам как раз поможет наше ключевое слово inline. Давайте посмотрим, во что превратится наш пример, если мы добавляем слово inline. А превращается он в следующее.
Самое интересное изменение произошло с функцией factoryCycle. Как мы видим, никакого объекта больше не создается. Мы просто делаем всё что хотели сделать прямо внутри кода функции. То есть получаем или трактор, или феррари, или ладу, вот и всё. Это не более чем оптимизация ресурсов для функций высшего порядка. Это и есть магия инлайн функций.
Давайте теперь перейдем к тому нужно это или нет, и если нужно, где это всё использовать. Во-первых, сама jvm имеет огромное количество оптимизаций и в том числе наделена умением инлайнить функции если это необходимо. И в целом вам студия может даже подсказать что эту функцию инлайнить не нужно.
Поэтому не надо это применять просто везде подряд. Как минимум, у вас должна быть функция высшего порядка. Дальше, как сказал один очень хороший человек (Саша, привет): "Профайлер, профайлер и еще раз профайлер". Это чертовски верно и в целом касается не только этой статьи, а вообще. Видео и статью про профайлер и как все это замерять я тоже однажды сделаю. Так вот, если вы точно не уверены, что вам нужно встраивание функции, то возможно не стоит применять инлайнинг. Тот же хороший человек сказал следующее: "Использовать инлайн нужно только, если вы хорошо понимаете, что вы делаете, потому что в большинстве случаев JVM инлайнит лучше, чем это делает программист, и он это точно проверял".
В котлине есть ещё один классный механизм, но это уже совсем для хардкорников, которые точно-точно понимают что делают. Как мы уже выяснили, лямбда у нас трансформируется в объект, и если вы хотите чтобы при компиляции инлайн функции у вас лямбда гарантировано оставалась объектом, а не вставлялась в место вызова, то вы должны написать аннотацию noinline перед вашей лямбдой.
Non-Local Return
Одной из самых важных фишек в inline функциях в Котлине — так называемый non-local return. В обычных случаях, когда мы используем лямбду, мы не можем себе позволить написать внутри lambda return и завершить этим работу функции, внутри которой мы вызвали лямбду.
В коде выше вы видите, что мы просто не можем использовать return внутри ordinaryFunction, но мы можем использовать return@ordinaryFunction. То есть, поставив лейбл лямбды, мы можем остановить работу выполнения лямбды. Происходит это как раз потому, что лямбда в целом это отдельный объект со своей функцией внутри, и логично, что мы не знаем, где будет вызван объект. Однако, inline функция встраивается в код в месте вызова, и получается, что как бы самой лямбды внутри inline функции как бы не существует.
Можем ли мы в таком случае указать return в произвольном месте функции? Конечно можем. Поэтому если мы помечаем функцию inline, то return будет работать не только на лямбду, но и на саму функцию куда эта ламбда встраивается. Это очень круто. Такие вот возвраты и называются non-local returns, потому что мы расширяем наши return до всей функции.
Вообще inline функции дают нам фактически возможности полноценного ФП языка, но при этом расходуют ресурсы как стандартный подход без функций высшего порядка и это очень помогает развиваться языку.
Reified
Также, вы наверняка встречали в коде такое слово и оно очень часто встречается рядом с inline функциями, как reified, обычно идет что-то вроде reified<T>. Давайте разбираться что же это такое тоже. Здесь все тоже достаточно просто. Иногда в качестве параметра в функцию нам надо передать не конкретный тип, например, Int, а класс этого типа. Выглядит это примерно как в коде ниже.
Зачем это вообще может понадобиться? Предположим вы хотите как-то поработать с рефлексией и вам для этого нужен именно тип класса, то есть не просто самое значение класса, а его тип, то есть KClass . Соответственно, если у вас обычная Generic функция, вы не сможете этого сделать просто потому, что конкретный тип параметра у вас будет определён в момент вызова этой функции и для этого вам нужно прямо параметром передать тип этого класса. Это работает, но выглядит так себе. Хотелось бы обойти без явной передачи параметра.
Для этого и нужно слово reified, которое позволяет вам работать с вашим дженериком будто это обычный класс. То есть вы можете применять операторы сравнения такие как is и as. Вот просто сравните.
Было:
Стало:
Но вы спросите: "А причем тут inline вообще? Почему мы видим их рядом?" Всё дело в том, что reified вы можете использовать только вместе с inline функциями. Если вы указываете у функции inline, вы как бы говорите компилятору встроить код вашей функции во все места, где вы её вызвали. Когда вы вызываете inline функцию с reified типом, компилятор уже может знать какой тип используется в конкретном месте вызова и соответственно p is T превращается в p is String. Поэтому они и используются только вместе. Ещё одна тайна раскрыта.
Здесь есть один очень важный момент. В случае, когда мы делаем reified параметр, мы больше не работаем с рефлексией, мы работаем с типом объекта напрямую, что конечно же увеличивает скорость работы со всеми вытекающими.
Рассмотрим еще один оператор и перейдем к inline классам. Представим ситуацию, что мы передали в нашу inline функцию лямбду, но вызвали её не прям внутри нашей функции, а внутри локального объекта внутри этой функции или другой вложенной функции внутри нашей функции. Понимаю как звучит, давайте на примере ниже.
Здесь мы видим в качестве параметра передана функция, но вызываем её внутри Runnable, что является реализацией типа object. Но так же мы могли дернуть наш body внутри вложенности внутри функции f. В таком случае нам поможет оператор crossinline. Как сказал мне все тот же хороший человек (Если не поняли, речь о Саше Нозике из Котлин коммьюнити): "Использование crossinline почти всегда убивает всю оптимизацию от использования inline функции". Так что где вам это пригодится в андроиде, я не знаю, но знать об этом всё равно полезно.
Здесь я хотел бы вставить комментарий пользователя kilg k из Youtube:
Добавлю комментарий про crossinline. Тут неважно, производительно оно или нет. Вообще сам факт передачи лябды в другой контекст очень напоминает ту часть видео, где один человек засовывает другому человеку в отверстия неорганичные предметы различных габаритов. Если смотрящий без отклонений - то смотреть неприятно. Проблема в том, что если все же передать лямбду в другой контекст, то return без лейбла уже сработает не так, как ожидается. И crossinline просто говорит компилятору (и синтаксическому анализатору), что в crossinline лямбде уже не работает разрешение просто использовать return, обязательно нужен будет лейбл. Или тег? Чот запамятовал( По факту, это всего лишь не дает пишущему лябмду застрелиться, показывая, что функция уходит в другой контекст, ну и сигнализируя среде разработке, чтобы там подчеркивалось с ошибкой return без лейбла. Соответственно, компилятор тоже не скопилит такую дичь с return без лейбла)
Inline Class
Несмотря на общее название с функциями, работают они совсем иначе. На каком-то из стримов даже показывал как это работает. Я даже слышал, что в 1.5 inline классы начнут называться value классы, но это не точно. Я думаю здесь гораздо лучше покажет все практика и в плане где это можно использовать и в плане как это работает. Практику можно увидеть в видео, которое я прикрепил сверху
Всем огромное спасибо и увидимся в следующих выпусках!
Подписывайтесь на Patreon, чтобы получать доступ к эксклюзивному контенту! - https://www.patreon.com/mobiledeveloper
Я в Youtube - https://youtube.com/c/MobileDeveloper
Я в Telegram - https://t.me/mobiledevnews
Я в Instagram - https://instagram.com/nplau
Я в Clubhouse - @neuradev