Написать пост

Kotlin и функциональное программирование: сделайте код лучше

Аватарка пользователя Ирина Тюльпакова

Урс Питер на KotlinConf 2023 объяснил, какие принципы сделают код функциональнее, рассказал про монады, контейнеры и библиотеку Arrow.

Урс Питер, сеньор software developer поделился лучшими практиками программирования на Kotlin. 

Превью видео Ed3t4WAe0Co

В видео он рассказал:

  1. В чём заключается основные принципы императивного и ориентированного на выражения программирования.
  2. Что такое монады и как их использовать.
  3. О пользе скоуп функций, функций высшего порядка и композитных функций.
  4. Представил Arrow - библиотеку для Kotlin, которая реализует концепции категорной теории и предлагает расширенные возможности для функционального программирования.
  5. Продемонстрировал, что важно выборочно использовать монады, так как они могут как улучшить, так и усложнить код.

Ниже — транскрибация ролика на русском языке.

Что ж, моя презентация будет посвящена функциональному программированию и Kotlin. Краткое введение о себе.

Я Урс Питер, сеньор software developer. «Днём» я работаю по специальности. Также  являюсь тренером в Академии Xebia, где помогаю разработчикам на всех этапах в Kotlin от новичка до среднего и продвинутого уровня, что довольно интересно, потому что я сталкиваюсь со многими средами и вижу, как Kotlin используется разными способами. Я также сертифицированный тренер Chatbrain. Карьеру я начинал около 20 лет назад с Java, затем около 10 лет назад перешел на Scala, вложил значительные средства в экосистему Scala, также был одним из первых сертифицированных тренеров, написал множество приложений на Scala, создал учебные материалы.

Но уже примерно четыре-пять лет я пишу исключительно серверную часть, по крайней мере, на Kotlin. К чему я это? К тому, что именно опыт работы со Scala побудил меня выступить с этим докладом. И это во многом связано с этим графиком.

Kotlin и функциональное программирование: сделайте код лучше 1

Итак, когда вы смотрите на временную шкалу, в 2017 году Kotlin начал стремительно расти, но в то же время начался спад Scala. И вопрос, конечно, в том, почему это так? Нет ни одной резонной причины, но я думаю, что в этой презентации я коснусь довольно важных деталей, почему это произошло. И, конечно, мы не хотим, чтобы это повторилось с Kotlin. 

<...>

Стили программирования

Тогда, что ж, давайте отправимся в путь и начнем наше путешествие, которое, надеюсь, будет преобразующим. И начнем мы с болот императивного программирования. <...>

Почему? если бы вы классифицировали стили программирования, которые у нас есть, и выделили бы только две большие группы, мы могли бы сказать, что, с одной стороны, у нас императивное программирование. Оно в основном основывается на объявлениях переменных, которые затем изменяются по ходу дела.

Kotlin и функциональное программирование: сделайте код лучше 2

Как вы видите в этом конкретном примере. Языковая функция, которую мы используем в Kotlin для реализации этого стиля, — это var`s, циклы, множество изменяемых вещей, такие как изменяемые коллекции, изменяемые объекты, операторы return.

Итак, как только мы достигли определенного этапа, мы думаем: хорошо, теперь все достаточно хорошо. Это императивное программирование.

Kotlin и функциональное программирование: сделайте код лучше 3

Напротив, у вас есть вторая категория, которая является программированием, ориентированным на выражения (Expression-Oriented Programming). Это еще не функциональное программирование, но оно лежит в его основе. И программирование, ориентированное на выражения, больше опирается на мышление функциями с точки зрения ввода и вывода. Функции, которые мы используем, — это более неизменяемые функции Kotlin, такие как классы данных, неизменяемые коллекции.

И конструкции, которые мы стараемся использовать, являются конструкциями выражений, что означает, что мы всегда ищем концепции, которые возвращают значение, а не только производят побочный эффект. И это, конечно, также относится к функциям более высокого порядка. 

Kotlin и функциональное программирование: сделайте код лучше 4

Примеры, которые вы видите здесь, в точности совпадают. Они дают точно такой же результат. Итак, что же на самом деле является фундаментальным в этих двух стилях программирования? 

Что ж, императивное программирование опирается на изменяемые данные наряду с побочными эффектами. Именно так мы думаем о нашей логике программирования, в то время как программирование, ориентированное на выражения… Оно как бы поддерживает представление о преобразованиях данных на основе некоторого ввода, который затем передается в некоторый вывод, и этот конкретный ввод может снова служить входными данными для следующего преобразования. Это действительно фундаментально для этих двух стилей. Итак, как только я лично начал осваивать экспрессионно-ориентированное программирование, я действительно стал лучше как программист, в том смысле, что я написал более качественное программное обеспечение, которое лучше поддерживается, более надежное и так далее.

Kotlin и функциональное программирование: сделайте код лучше 5

Почему это так? Ну, это довольно легко обосновать. Итак, во-первых, когда вы просто смотрите на приведенный здесь пример, там меньше строк, он более лаконичный. Это также более понятно, потому что в основном мы программируем на более высоком уровне абстракции. Вместо циклов и изменения переменных мы теперь очень четко выражаем наши намерения, говоря, что мы фильтруем, мы максимизируем. Таким образом, это придает нашему коду больше семантики.

Поскольку у нас есть ввод-вывод, мы также более детерминированы, верно? Потому что мы получаем выходные данные, а когда у вас есть выходные данные, нам легче их протестировать. И что также интересно отметить, так это то, что при программировании, ориентированном на выражения, наши блоки области видимости (scope blocks) меньше, чем при императивном стиле.

В императивном примере вы видите, что мы объявляем эту переменную devs сверху, а затем на протяжении всей процедуры мы используем её. Таким образом, область применения довольно велика, в то время как в стиле, ориентированном на выражение, у нас в основном есть только функция, которая является областью применения, а затем мы переходим к следующей области применения, что также значительно упрощает рассуждения о разработке программного обеспечения, о нашем программном обеспечении. Итак, если вы еще не полностью освоили этот стиль программирования, ориентированный на выражение, то я советую сделать это.

Попробуйте хотя бы две недели. И потом, я совершенно уверен, что если вы по-прежнему будете использовать более императивный стиль, вы от него уже не уйдёте. Потому что когда вы выберетесь из болот императивного программирования, вы окажетесь в плодородной долине программирования, ориентированного на выражения. Ладно, пока все хорошо, что ж, вы можете подумать, что звучит все это мило, но реальный мир не всегда так идеален, как мы только что описали, потому что у нас есть много API, которые все еще очень изменчивы, так как же мы можем справиться с этими ситуациями? 

Функции области видимости (scope functions)

Сначала давайте посмотрим на пример, чтобы увидеть, что мы более или менее делаем. Итак, очевидно, мы инициализируем некоторые клиенты REST, а также назначаем некоторые свойства для выполнения нашего вызова. Затем мы что-то делаем с файлом.

Kotlin и функциональное программирование: сделайте код лучше 6

В конечном итоге мы извлекаем данные с помощью этих REST-клиентов, которые затем выводим в какой-нибудь CSV-файл, и в конце концов закрываем наш ресурс. И поскольку эти API изменяемы, вы можете подумать «хорошо, единственный способ, которым я могу решить эту проблему, — это объявления переменных, а затем попутно менять их». Ну, это не совсем так. 

Потому что в Kotlin есть эти замечательные функции области видимости, и, применяя их, мы можем добиться большего. Это был бы пример со скоуп функциями. Итак, что мы получаем с помощью скоуп функций?

Kotlin и функциональное программирование: сделайте код лучше 7

Прежде всего, теперь с помощью apply мы группируем все операции, которые ...вызывают изменения в нашем объекте. Таким образом, он, по крайней мере, изолирован. Мы изолируем нашу изменчивую часть. С помощью let мы можем опустить оператор getAll и return. Кроме того, это приятно, потому что теперь мы четко отмечаем побочный эффект.

Итак, когда вы читаете этот код, вы говорите: "О да, здесь имеет место побочный эффект". Кроме того, пример с файлом и run является прекрасным примером изоляции в том смысле, что мы группируем операции с файлом в блок, но мы даже не раскрываем файл. В примере слева у нас была файловая переменная, которую мы можем использовать везде. Поэтому, когда мы позже проведем рефакторинг, вы можете легко сделать что-то с файлом, случайно весь его изменить, в то время как справа это действительно применимо только в этом блоке, а не за его пределами.

Функции высшего порядка (higher-order&nbsp;functions)

Итак, что мы получаем с помощью этих скоуп функций, так это способ как бы соединить императивный мир с миром, более ориентированным на выражение. Итак, имея это в виду, я думаю, мы продвинулись немного дальше в нашей плодотворной долине. Но что ж, когда вы оглядываетесь вокруг, горы все еще есть, верно? И мы еще не там.

Мы все еще в долине. Итак, давайте посмотрим, сможем ли мы подняться наверх, чтобы полюбоваться прекрасным видом. Итак, мой клиент подходит к моему столу и спрашивает: "Приятно, что вы выводите мои данные в CSV, но мне нужен гораздо более навороченный формат. Я также хочу иметь TSV, отдельные значения в табуляции.

Kotlin и функциональное программирование: сделайте код лучше 8

Вот как мы могли бы решить эту проблему, верно?

Что, конечно, довольно глупо. Мы просто продублировали все это, изменили наши выходные данные на TSV и создали проблему с ремонтопригодностью. Итак, как вы можете лучше решить ее?

Kotlin и функциональное программирование: сделайте код лучше 9

Это довольно просто сделать. Итак, что мы можем сделать в первую очередь, так это определить часть, которую мы дублировали. И эта дублированная часть, по сути, становится нашей общей структурой управления. Затем мы идентифицируем изменяющуюся часть.

И мы очень внимательно смотрим на то, что входит и что выходит. И эту часть мы выводим в функцию. А затем используем эту функцию в указанном месте, чтобы, ну, теперь быть очень гибкими. Итак, в этом преимущества функций более высокого порядка. Я часто вижу, что функции более высокого порядка используются в коллекциях и, возможно, в других API, но я не всегда вижу, как они используются в вашем собственном бизнес-коде.

В то время как есть много случаев, когда у вас есть какая-то структура управления, которую вы хотите повторно использовать и просто подправить определенную ее часть, именно здесь функции более высокого уровня действительно могут отлично справиться. Как пример в тестировании, с которым я часто сталкиваюсь. Тестирование часто не является тем местом, где вы применяете лучшие инженерные практики, поэтому дублирование не считается таким злом, как в реальном бизнес-коде.

Kotlin и функциональное программирование: сделайте код лучше 10

Итак, вы видите этот код довольно часто, по крайней мере, там, где я бываю. Например, здесь мы создаем некоторые тестовые данные, а затем очищаем их. Вот пример использования, который у нас здесь есть.

И здесь применим тот же трюк, выводим общую структуру управления, преобразуем изменяющуюся часть в функцию, и тогда мы получаем два преимущества. Прежде всего, у нас было повторное использование. Мы даже можем сделать нашу функцию немного настраиваемой, например, сказать, что я хочу создать так много элементов для тестирования.

Kotlin и функциональное программирование: сделайте код лучше 11

Итак, это, во-первых, повторное использование, а во-вторых, также наши тесты становятся более выразительными, потому что теперь мы можем сосредоточиться на условиях тестирования, а не на всей аппаратуре, необходимой нам для получения определенного фрагмента данных в состоянии, при котором мы можем его протестировать. 

Хорошо. Итак, как только мы освоим функции высокого порядка, мы окажемся на изобильной высоте функций высокого порядка, которая все еще не достигла вершины.

Композитные функции (composable functions)

Что ж, если вы внимательно посмотрите на эти иерархические функции, которые мы только что создали, то чего нам все еще не хватает, так это какой-то абстракции, чтобы объединить их вместе для создания новых ценных результатов, верно? Итак, чего нам, по сути, не хватает, так это того, что называется композиционностью.

Kotlin и функциональное программирование: сделайте код лучше 12

Эти вещи не складываются. Когда мы смотрим на самый первый пример, который мы видели в начале выступления, с коллекциями, мы видим, что это прекрасно складывается, не так ли? Итак, у вас есть коллекция, затем у вас есть эти функции более высокого порядка, которые вы применяете, вы выполняете свои преобразования, пока не получите желаемый результат.

Очевидно, контейнер коллекции или объект коллекции для сбора, вместе с функциями более высокого порядка, дает нам эту возможность. Так что было бы довольно интересно сделать еще один шаг вперед и взглянуть на фундаментальные аспекты, которые позволяют нам поддерживать композиционность.

Потому что, если бы мы могли как бы вывести эти существенные аспекты, мы смогли бы создавать наши собственные структуры данных помимо коллекций, которые мы можем составлять.

Теория категорий

Что ж, и если вы попытаетесь достичь этого, то вот куда вы направляетесь, в область теории категорий. Что такое теория категорий? Теория категорий — это теория математических структур и их отношений. Мне нравится сравнивать ее с периодической таблицей Менделеева в физике, знаете, где у вас есть все атомы, и все атомы вы также можете объединить в молекулы, которые обладают определенными чертами и характеристиками.

И третья категория — это нечто похожее, но с логическими строительными блоками, которые вы можете комбинировать, пока не получите определенные характеристики. И, как вы можете видеть, это не высота, не холм, это гора. А в горах можно подняться очень высоко, но можно и упасть очень глубоко. Итак, это опасная зона, где внезапно появляются все эти модные слова. Ну, а почему это опасная зона?

Я думаю, во многом это потому, что терминология, используемая в категориальной теории, которая не обязательно имеет отношение к разработке программного обеспечения, просто используется в разработке программного обеспечения, потому что это хорошая область ее применения. Но эти термины действительно своего рода притянуты за уши. 

<...>

Контейнеры

Хорошо, итак, когда я спросил в начале, кто слышал слово «монада», поднялось довольно много рук, но определенно не все, верно? Приятно то, что мы все используем монады, и даже на ежедневной основе.

Kotlin и функциональное программирование: сделайте код лучше 13

Очевидно, вы используете его, сами того не подозревая. Мне нравится думать о монадах по-разному, и есть разные способы, этот способ помогает мне лучше всего — думать о них как о контейнерах. Что такое контейнер? Ну, контейнер — это то, что содержит предметы. Интересная особенность контейнеров в том, что у вас есть разные виды контейнеров. У вас есть герметичные контейнеры, у вас есть контейнеры с кондиционером, у вас есть безопасные контейнеры. Несмотря на то, что контейнеры содержат что-то общее, они все равно могут быть разной формы. Итак, что мы попытаемся сделать сейчас, так это взглянуть на контейнер коллекций. Коллекции также содержат элементы, верно?

И на что мы действительно смотрим, так это на то, что такая коллекция делает полезной с точки зрения композиции? Итак, не все эти функции высшего порядка, но с точки зрения компонуемости, каковы ключевые характеристики, которые делают это возможным? Хорошо, давайте начнем с первого шага. И все это довольно просто, вы можете быть удивлены.

Kotlin и функциональное программирование: сделайте код лучше 14

Вероятно, только при сочетании вещей они становятся немного сложнее. Итак, первый пример, который вы видите, — это то, что мы можем комбинировать коллекции. Когда у нас есть контейнер с элементами, и мы объединяем его с другим контейнером с элементами, мы получаем новый контейнер, содержащий все элементы предыдущих. И когда вы затем переходите к этой категории, горе, скала, тогда можно назвать это моноидом.

У монады есть две характеристики. Она может создавать пустую вещь. Вы можете создать пустой список, верно? И вы можете объединять списки. Но когда вы объединяете списки, вы не получаете как бы два контейнера. Нет, вы по-прежнему получаете один контейнер, который содержит события другого. И просто чтобы сделать это немного сложнее, в основном, комбинируемая часть называется полугруппой.

Но ладно, пока давайте просто придерживаться термина “монада” (я бы предпочел термин "комбинируемый"; тогда я бы вроде как понял, о да, вы комбинируете разные вещи). 

Kotlin и функциональное программирование: сделайте код лучше 15

Итак, это первая характеристика. Вторая заключается в том, что вы можете преобразовывать элементы коллекции. У нас снова есть контейнер, содержащий элементы. Мы берем каждый элемент, применяем к нему определенную функцию, результатом чего затем будет новый контейнер с преобразованными элементами. И если у вас есть такая возможность, то объектам удается вызвать это функтором. Итак, когда у чего-то есть метод map, это функтор. «Сопоставимый» (mappable) для меня звучало бы более логичным.

Kotlin и функциональное программирование: сделайте код лучше 16

Хорошо, тогда третий вопрос, который, вероятно, самый важный, самый запутанный, заключается в следующем. Давайте посмотрим на эти простые домены, у меня есть разработчик, у которого есть языки, и если я хочу знать все языки нескольких разработчиков, тогда, если я буду использовать map, я получу этот список списков, верно? Но, в конце концов, я просто хочу иметь список языков.

Итак, что я должен применить? Вот тут-то и появляется flatMap, так что же делает flatMap? Каждый элемент, который вы извлекаете из контейнера, снова преобразуется в контейнер, но результирующий контейнер как бы выравнивает (убирает) дополнительный контейнер, который мы добавили.

 Все в порядке, вы получаете только один список с элементами. И если вы затем объедините характеристики, которые мы видели раньше, то есть моноид и функтор, в сочетании с flatMap, то та-да, вот и наша монада. Итак, я сказал, что монада — это комбинация моноида, который я сейчас называю комбинируемым, функтора, отображаемого аспекта и монады, я бы предпочел термин «компонуемый», потому что это то, что вы можете делать с этими вещами. Хорошая ли идея дать этому новую терминологию?

Честно говоря, я думаю, что это плохая идея. Поэтому я думаю, забудьте об этом, придерживайтесь моноидной строки и так далее. Но, ну, определенно, не помогло то, что они использовали такого рода терминологию. И также здесь, на самом деле, больше уровней абстракции. Моноид идёт от «применимый» (applicative) и так далее. Есть также небольшие нюансы, но пока давайте просто придерживаться этих трех. Итак, мы все знаем монады, верно?

Монады (Monads)

Потому что вы используете коллекции. И я уверен, что вы знаете больше монад. Например, optional. Что такое optional?

Kotlin и функциональное программирование: сделайте код лучше 17

Если вы попытаетесь определить, что делает монада, есть какое-то особое житейское определение, которое вы можете применить к ней. Которое выглядит следующим образом: «Опциональность моделирует эффект опциональности» («an optional models the effect of optionality»). Итак, эффект опциональности.

Это очень важная часть, и мы также увидим позже, что это слово будет повторяться. Итак, давайте посмотрим, является ли опциональный параметр монадой? Давайте проверим. Есть ли у нас пустой метод? Мы можем создать пустой опциональный параметр, который является числом, верно? Можем ли мы сопоставить наши значения?

Да, мы можем. Итак, если опциональный параметр содержит значение, мы можем сопоставить его. И если он пуст, он ничего не делает. Здесь вы видите эффект сопоставления.

Kotlin и функциональное программирование: сделайте код лучше 18

Отличается от коллекции, но это все равно map с другим эффектом, то же самое верно и для flatMap, поэтому, если на flatMap я создам наши опции, я не получу опциональный набор опций (option of optionals), потому что один слой будет выровнен… Кто занимался Reactive программированием с фьючерсами, всеми этими абстракциями? Да, хорошо, круто, довольно много. Там тоже есть монады.

Что они делают? Ну, моноблок или реактивный блок моделирует эффект синхронности. Можем ли мы создать пустой?

Можем. Итак, как здесь появляется map? Что делает map? Здесь у нас есть некоторый API, который возвращает моноид (mono), и map в основном будет вызван, как только асинхронная обработка будет завершена, и мы получим значение «окей». Здесь вызывается map.

То же самое верно и для flatMap, поэтому, если я хочу последовательно вызывать реактивный блок, то есть сначала API 1, затем API 2, я должен использовать flatMap, потому что в противном случае я получил бы моноид из моноида. Итак, вы также видите здесь, что эффект map и flatMap полностью отличается от других монад, которые мы видели до сих пор, но это все равно map и flatMap.

Kotlin и функциональное программирование: сделайте код лучше 19

Так для чего же хороши эти контейнеры монад? Ну, все эти абстракции, я действительно думаю, что плохое в этом смысле уже произошло, так что вы не можете повернуть время вспять, но в основном это просто отпугнуло людей. Это недостаток. Но что вы получите, если пройдете через это, так это, ну, возможность компоновки. И поскольку мы знаем, что можем компоновать материал, что ж, давайте попробуем это.

Kotlin и функциональное программирование: сделайте код лучше 20

Давайте составим что-нибудь. И хорошим примером использования компонуемости может быть обработка ошибок. У вас здесь есть эти перехваты попыток, и, что ж, на самом деле это некомпонуемая конструкция, верно? Своего рода языковые функции, которые нельзя комбинировать. Просто они должны использоваться несвязанно, по-разному.

Kotlin и функциональное программирование: сделайте код лучше 21

Ну, если мы хотим что-то скомпоновать, тогда нам нужна монада, и хорошей новостью является то, что в стандартной библиотеке Kotlin уже есть такая монада, которая называется result и выглядит результат. Result моделирует эффект наличия значения «окей» или исключения, и мы можем применить результат выглядит следующим образом: у него есть map и flatMap, так что, по-видимому, это монада. Это был код раньше, теперь мы используем runСatching в клиентском коде, который затем не вернет нашему разработчику результат либо хорошего значения, либо исключения, и с этого момента мы можем начать компоновать. Это довольно хороший вопрос, не так ли? Итак, у нас есть этот клиент GetDevByName. Откуда мы знаем, что можем ожидать исключения? Этого нет в подписи. Контракт не говорит мне, что есть исключение. И это очень интересная дискуссия, которая, в некотором смысле, то, что мы здесь видим, не является прозрачным.

Мы можем вернуть то, чего нет в нашем контракте. И функциональным людям это не нравится. Они считают, что прозрачность важна.

Kotlin и функциональное программирование: сделайте код лучше 22

Напротив, довольно интересно, что в Kotlin есть только непроверямые (unchecked, те, которые не отлавливаются компилятором  — прим.переводчика) исключения. Так что, по-видимому, есть некоторые, да, и это немного не согласовано. Не будет рассуждать, хорошо это или плохо. Так что давайте просто продолжим исследование, и я думаю, тогда мы сами придем к выводу. Предположим, что мы хотим быть прозрачными. А если мы хотим быть прозрачными, тогда мы должны быть откровенны. Итак, мы должны четко отметить, что этот метод также может выдать вам исключение посредством результата. И что ж, это то, что мы должны сделать. Таким образом, все наши методы теперь относятся не к языку как к возвращаемому термину, а к результату языка. Тогда мы можем творить нашу композиционную магию. Хорошо, давайте расширим наш пример и используем третий вызов API.

Kotlin и функциональное программирование: сделайте код лучше 23

И если мы попытаемся вызвать эти три API последовательно, сначала один, используя выходные данные в качестве входных данных, то в итоге мы получим вот такой код. Таким образом, задействовано много flatMaping. В начале я сказал, что бываю во многих местах, где обучаю разработчиков Kotlin, но объяснять это, особенно тому, у кого очень сильный опыт работы с императивами, довольно сложно. Я имею в виду, что коллекции плоских отображений уже немного сложно. Но контекст flatMaping отличается от контекста других структур? Я имею в виду, это не так уж сложно, но все равно, ощущения не очень приятные.

Итак, вопрос, конечно, в том, есть ли способ лучше? Я думаю, сейчас тот момент, когда я представляю вам Arrow.

Что такое Arrow? Arrow — это действительно красиво созданный API, который фактически реализует всю эту теорию, категорию, парадигму, а также предлагает большое разнообразие монад. Я действительно считаю это жемчужиной, что не всегда было так, потому что вначале это был порт из Scala. В Scala есть этот материал в большем количестве, довольно много библиотек доходят до крайности с такого рода концепциями, и они были перенесены на Kotlin. Самая первая версия была как бы не связана с идиоматическим Kotlin. У вас был Kotlin, а затем у вас был этот функциональный мир, и вам действительно приходилось выбирать между одним и другим. 

Они не очень хорошо сочетались. И действительно, самое замечательное в том, что сделала Arrow, особенно в релизе, который вышел сейчас, 2.0, заключается в том, что им удалось действительно блестящим образом соединить два мира, так что большая сложность, которая обычно бросалась бы вам в глаза, исчезла, и вы действительно можете воспользоваться преимуществами. Я приведу несколько примеров этого. Итак, в основе Arrow лежит эффект. Если вы помните, мы всегда говорили, что опциональная модель — это эффект чего-либо.

Kotlin и функциональное программирование: сделайте код лучше 24

Эффект — это основная вещь, и затем у вас есть монады, которые реализуют определенный эффект, например опциональный. Результат также преобразуется в эффект. У вас также есть either, вы, возможно, слышали об этом типе, either немного мощнее, чем result. Result может содержать только хорошее значение и исключение. Either просто может содержать два значения, может содержать одно значение разной формы. Таким образом, он может иметь значение типа A или значение типа B, и только по одному за раз.

Обычно правое ассоциируется с хорошим результатом, а левое — с плохим, но это не обязательно так, но часто это применяется именно таким образом. Есть validated, где вы можете выполнить некоторую накопленную проверку <...>. Хорошо, что Arrow также предлагает очень классную функцию, которая называется monad comprehensions. С ней мы можем работать лучше, чем с помощью flatMap.

Monad comprehensions

Kotlin и функциональное программирование: сделайте код лучше 25

Как это работает? Что важно упомянуть, так это то, что монопонимание — это нечто из функциональных языков, а у функциональных языков для этого есть языковые особенности. В Kotlin нет языковой функции для монокомпонентов, но в Kotlin есть эта функция, литерал которой связан с receiver, верно? Это то, что вы видите здесь, result, и тогда receiver в этом случае будет такой областью действия result, которая для вас как бы скрыта. Но у этого есть некоторые методы расширения, которые являются bind, и этот метод bind затем может вызывать монады внутри вашего блока кода. Итак, здесь вызов bind и то, что делает bind, — это либо дает вам значение, если вы вычисляете правильное значение, и если это было бы исключением, оно просто провалилось бы и просто вернуло результат, содержащий исключение.

Таким образом, вычисления сразу же прекратились бы. Таким образом, вам не нужна эта вложенность в flatMap для достижения вашей цели. Это действительно блестящий способ по-прежнему использовать потенциал этих монад, но при этом не быть обремененным flatMapping. Я думаю, что это отличный подход.

Kotlin и функциональное программирование: сделайте код лучше 26

Более того, и это также то, что они вам скажут, так что это краткий обзор, заключается в том, что с появлением этих контекстных приемников вы даже можете сделать эти сигнатуры методов еще более совместимыми с идиоматикой Kotlin, в том смысле, что теперь они определяют только базовый возврат потока тип, поэтому языковая статистика и другие эффекты, которые я мог бы получить, как бы четко отделены от, ну, базового типа, использующего эти контекстные приемники. Я также думаю, что здесь это отличный способ быть откровенным, но не так, чтобы это слишком сильно мешало вам. Так что это определенно потрясающая разработка. Ладно, теперь я думаю, что эта компонуемость потрясающая.

Итак, давайте просто используем все, что можем. И поскольку мой вариант использования требует только более одной монады, почему бы просто не объединить эти вещи вместе? Потому что мне, во-первых, нужно какое-то реактивное поведение. Во-вторых, я хочу, чтобы код был прозрачным. И в-третьих, почему бы не использовать option?

Итак, вы получаете эти вложенные контейнеры. Если бы вы тогда только попытались вызвать эти три службы последовательно, это то, что у вас получилось бы в итоге.

Проблемы...

Kotlin и функциональное программирование: сделайте код лучше 27

И вы можете себе представить, что теперь мы только что ввели небольшую головную боль при обслуживании. Итак, все map и flatMap, которые мы видим здесь, вызываются одинаково, но я сказал, что эффект на самом деле разный. FlatMap монад отличается от map из result, отличается от map из optional, но это все map и flatMap. Это то, что вы видите.

Kotlin и функциональное программирование: сделайте код лучше 28

Так что все не так уж и здорово. Ну да, вы еще не занимались бизнес-логикой, и если вы обнаружите, что пишете такой код, тогда я могу поздравить вас, потому что вам удалось превратить свою кодовую базу в контейнерный терминал, и если вы внимательно посмотрите, где вы находитесь, то это здесь, у этого крана, и вы просто перемещаетесь вокруг этих контейнеров и пытаетесь найти [что-то], заворачивайте и разворачивайте их, чтобы добраться до того, что вам нужно. Потому что в основном мы программируем обеспечение, которое создаем, что действительно интересно — это то, что находится внутри контейнеров, а не сам контейнер. И это именно то, что у меня много раз было со Scala, и я действительно знаю, что у многих других было то же самое, что… Это очень неприятное чувство в том смысле, что я хочу создать бизнес-логику для выполнения моих вариантов использования, но я всегда борюсь только с упаковкой, распаковкой, заглядыванием в эти контейнеры. Итак, мы только что видели, что у вас есть эти представления о монадах, возможно, они и здесь могут помочь.

... и их решение

Извините, плохие новости. Понимание монад работает только для одного уровня монады. Так что, если у вас есть вложенные монады, это не сработает. Как насчет трансформаторов монад? Что такое трансформаторы монад? Это снова своего рода грязный трюк для преобразования значения внутренней монады во внешнюю. Итак, вместо того, чтобы вызывать map, flatMap, теперь вы можете вызвать mapT. И тогда вы преобразовали бы комбинацию mono result напрямую в convert result. Ну, это работает только для двух уровней.

Kotlin и функциональное программирование: сделайте код лучше 29

У нас их три, верно? И вы все равно не можете использовать для этого что-то более всеобъемлющее. И поверьте мне, если вы пойдете по этому пути, то получите такого рода методы: mapТ, mapТ, mapТ, внешняя flatMap, внутренняя. Я написал такой код.

Kotlin и функциональное программирование: сделайте код лучше 30

Как только это было сделано и мой тест сработал, на следующий день код перестал работать, не говоря уже о том, что мои коллеги не поняли, что происходит. Это действительно не то, что вы хотели бы получить. Итак, совсем не весело. Хорошо, значит, монада совсем не хороша? Что ж, если вы ограничитесь одной монадой, я думаю, у вас все будет в порядке.

Kotlin и функциональное программирование: сделайте код лучше 31

Но как я могу свести три к одному? Итак, давайте попробуем. Я видел, как некоторые из вас все еще поднимают руки, которые использовали эти реактивные строительные блоки. Кто их использует до сих пор? Хорошо, не многие. Я думаю, это хорошо, потому что, если вы это сделаете, я действительно настоятельно советую вам взглянуть на со-программы. Они могут значительно облегчить вашу жизнь, но при этом дают вам точно такие же характеристики, которые вы получаете с помощью этих реактивных строительных блоков.

Kotlin и функциональное программирование: сделайте код лучше 32

Так что настоятельно рекомендую обратить на них внимание. Итак, мы преобразуем их в spend и можем выбросить наш mono. Второй, ну, вариант. Я думаю, у нас уже есть эта конструкция в Kotlin, верно, которая является nullability. Зачем вводить другой тип конструкции для использования возможности обнуления? Вы же не собираетесь использовать необязательный параметр Java в своем коде, верно?

Во всяком случае я не советую вам так делать. Просто придерживайтесь одной концепции опциональности, которая в Kotlin ещё и nullability, и все. Хорошо, и как только мы это сделаем, я думаю, наш код снова станет в некотором роде управляемым. И если вы используете let или если вы затем используете другие гласные, которым вы присваиваете свои результаты, это зависит от вас. Но тогда с пониманием результата, комбинацией с методами привязки и приостановки, возможностью обнуления лучше подойдёт. Ладно, я думаю, теперь мы прошли через этот монадический туман, просто взглянули на вершину.

Если бы я остановился прямо сейчас, я думаю, мы все равно были бы не очень довольны, верно? Должен же быть какой-то синтез целого. Итак, давайте попробуем, если у вас получится лучше. Что на самом деле подводит нас к следующему вопросу: какая монада хороша для вашего кода? Что подводит нас к более широкому вопросу: что такое хороший код?

Kotlin и функциональное программирование: сделайте код лучше 33

Что ж, об этом написано много книг, и еще много книг будет написано на эту тему. Мне хотелось бы, чтобы все было довольно просто. Пока что я видел, что хороший код отражает область, в которой вы находитесь. Итак, если вы зоомагазин, то речь идет о кошках, собаках, собачьем корме. Если вы финансовое учреждение, то речь идет об ипотеке, акциях и так далее. Другими словами, недоменные абстракции, которые вы используете в своем коде, на самом деле не выходят за пределы вашего домена, являются частью абстракций. И если у вас есть эти недоменные абстракции, что ж, тогда они действительно служат очень специфической, полезной службе. Другими словами, если ваш заказчик просматривает ваш код и понимает его, это то, что я считаю хорошей основой для создания кода, который будет поддерживаться многими поколениями, которые придут после вас. Другими словами, если ваш код выглядит как контейнерный терминал, это явно не очень хороший код.

За исключением случаев, когда ваша компания является контейнерным терминалом, верно? Тогда можно называть вещи «контейнерами» и менять их местами. Итак, теперь то, что я хотел бы сделать, это как бы дать вам представление о том, что произойдет, если вы просто добавите монады ко всему. Как это повлияет на цепочку вызовов вашего приложения.

Kotlin и функциональное программирование: сделайте код лучше 34

Используя очень простое приложение, так что никаких причудливых шестиугольников, но и там вы получили бы более или менее тот же эффект. Итак, у вас есть некоторый уровень доступа к данным, затем у нас есть некоторый уровень обслуживания, некоторый уровень API, я предполагаю, что обычно используется Spring Boot, и затем, ну, вам нужно обработать некоторые исключения. И что мы видим здесь, в данном конкретном случае, так это то, что мы откровенны, правы, прозрачны, мы передаем эти результаты, потому что хотим быть откровенными и прозрачными, но делаем ли мы что-то с этой прозрачностью?

Нет. Что вы делаете, так это в контроллерегенерируем исключение, чтобы наш обработчик исключений мог обработать его и вернуть ответ REST. Возможно, это могло бы вернуть результат в нашем контроллере. Да, мы могли бы заставить это работать, но мы ничего с этим не делаем. Итак, что ж, если в 95% случаев вы все равно их не используете, я обнаружил, что, обратившись к Scala, где мы действительно довели это до крайности, это просто работает лучше.

Kotlin и функциональное программирование: сделайте код лучше 35

И да, это не совсем чистый метод, но он работает лучше. И, конечно, у вас есть остальные 5%. Я думаю, это интересный случай. Итак, мы рассмотрим наши примеры, которые у нас есть.

Kotlin и функциональное программирование: сделайте код лучше 36

Итак, у нас есть наши основные потоки. Затем у нас есть определенные исключения, например, вы не можете получить доступ к своей базе данных. В любом случае, вы не сможете восстановиться после этого. Просто выбросьте его и верните свой ответ об ошибке. Но есть случаи, в основном в API, когда в зависимости от определенной ошибки API мы хотим восстановить ее в нашем сервисе.

И здесь действительно имеет смысл быть откровенным. Итак, в зависимости от того, что мы получаем , и мы могли бы использовать хороший floor или eternity flow, так что это означает , что для работы с базой данных я бы действительно не стал оборачивать это в монады, просто верните ему сущности, достаточно хороший API сказал, что в зависимости от того, что вы хотите сделать, я бы не стал использовать result кстати, я бы предпочел использовать либо потому что результат в случае, если у вас есть исключения, вам нужно было бы как бы извлечь исключения, чтобы внедрить их в свой бизнес-процесс для принятия других решений, которые в исключительных случаях не принимаются, верно? Поток управления, основанный на исключениях, немного противоречит шаблону. Но с помощью IDER мы можем просто определить тип ошибки, который может быть закрытым интерфейсом с некоторыми классами данных, и тогда мы сможем довольно легко обработать левую часть нашего IDER. Таким образом, поступая таким образом, мы получаем два преимущества.

Kotlin и функциональное программирование: сделайте код лучше 37

Прежде всего, мы получаем явный контракт. Мы видим, о да, мы можем вернуть именно эти ошибки. И у нас также есть эти операторы для обработки случая, когда мы получаем ошибку, так что мы можем довольно легко восстановить. И если все же произойдет что-то еще, что нас не волнует, выбросьте и позвольте универсальной обработке исключений просто выполнять свою работу. Итак, ключевая мысль здесь заключается в том, что если вы используете монады выборочно, то они действительно великолепны.

Итак, два примера, прежде чем мы подведем итог. Вот, например, проблема, с которой я тоже сталкиваюсь довольно часто. Чтобы создать какие-то объекты, вы должны проверить их на возможность nullability.

Kotlin и функциональное программирование: сделайте код лучше 38

Для различных переменных, прежде чем вы передадите их, в противном случае вы не сможете создать такую переменную Kotlin, потому что значения, которые она принимает, не являются null, это довольно утомительно, также здесь у Arrow есть отличная функция, которая заключается в понимании монады с нулевым значением, поэтому в Arrow типы с нулевым значением также считаются монадой, так что вы можете используйте это понимание, а затем вместо использования if-else просто использовать bind, и если одно из этих значений вернет null, то, что ж, весь результат будет null. Итак, я думаю, что это отличное дополнение к вашему коду, позволяющее предотвратить множество конструкций if-else. Еще одна прекрасная вещь — это решение этой конкретной проблемы.

Kotlin и функциональное программирование: сделайте код лучше 39

Итак, если у вас глубоко вложенные структуры данных и вы хотите что-то изменить на конечном уровне, и, конечно, мы предполагаем неизменяемые структуры данных, то это довольно утомительная задача — добраться до конечного уровня и преобразовать его, верно? Вы должны копировать, копировать, копировать. На различных конференциях я видел ужасные решения для этого, чрезвычайно сложные с компоновщиками и большим количеством кодирования только для решения этой проблемы. Также здесь Arrow использует так называемую оптику (optics).

Oxygen также основан на всех этих функциональных концепциях, и они действительно проделывают огромную работу, чтобы решить именно эту конкретную проблему. Таким образом, вы можете просмотреть график с тем, что вы видите в начале, затем вызвать modify, передать конкретный экземпляр, а затем скопировать только тот уровень, который вы хотите скопировать. Это может быть на каждом уровне иерархии вашей структуры данных. Это не то, что вы можете использовать просто так. Для этого вам нужно настроить плагин компилятора, потому что необходимо сгенерировать некоторый код.

[42:54] Вам также нужно аннотировать свои классы. Но это определенно стоит хлопот, если вы делаете это много раз в своем домене. Это действительно того стоит. Хорошо, итак, я сказал, что как только мы сможем не использовать монады в качестве золотого молотка, бросать его во все подряд, чтобы все наши цепочки вызовов были как бы загрязнены монадами, я думаю, что именно здесь мы достигли пользы функционального здравого смысла. Итак, где вы можете выбрать лучшее и просто пропустить остальное. 

Следите за новыми постами
Следите за новыми постами по любимым темам
2К открытий6К показов