28 июля в мире Android произошло важное событие: анонсировали Jetpack Compose 1.0. Вместе с этим нововведением места для ключевого слова class стало ещё меньше. Kotlin поддерживает парадигму функционального программирования (ФП), и разработчики Google этим умело пользуются.
Часто объектно-ориентированный подход (ООП) ставят в противовес ФП. Это ошибка: они не соперники и могут друг друга дополнить. Одно из понятий ФП — каррирование функций. Оно позволяет произвести частичное применение в ожидании данных, минимизируя количество ошибок в коде: это может пригодиться при разработке в Compose и не только :)
Давайте посмотрим, что такое каррирование и как его можно использовать на практике.

Несколько слов об аргументах функции
Для ООП-мира привычно думать, что функции могут иметь несколько аргументов. На самом деле это не так. Функция описывает связь между исходным (областью определения) и конечным множеством данных (областью значений). Она не связывает несколько исходных множеств с конечным множеством.
Таким образом, функция не может иметь несколько аргументов. По сути, аргументы — это множество:
// Привычный вид функции fun f(a: Int, b: Int) = a + b // Скрытый вид функции fun f(vararg a: Int) = a.first() + a.last()
Следуя соглашению, лишний код опущен. Но всё же это функция одного аргумента, а не двух.
Что такое каррирование функций
Итак, теперь мы знаем, что аргумент один — массив данных (кортеж). Тогда функцию f(a, b) можно рассматривать как множество всех функций на N от множества всех функций на N. Это выглядело бы так: f(a)(b). В таком случае можно записать: f(a) = f2; f2(b) = a + b.
Когда применяется функция f(a), аргумент a перестаёт быть переменной и превращается в константу для функции f2(b). Результат выполнения f(a) — функция, которую можно выполнить отложенно.
Преобразование функции в вид f(a)(b) и называется каррированием — в честь математика Хаскелла Карри. Хотя он это преобразование и не изобретал :)
// Пример каррированой функции с применением fun fun f(a: Int) = { b: Int -> a + b } // Пример каррированой функции с применением переменной val f2: (Int) -> (Int) -> Int = { b -> { a -> a + b } }
Jetpack Compose и частичное применение функций
Каррирование позволяет разбить функцию на несколько блоков и выполнить их отложенно по мере надобности. Для этого подхода существует термин — «частичное применение».
Приведу практический пример частичного применения в Jetpack Compose. Добавим простой BottomNavigation с логикой переключения в приложение, любезно сгенеренное нам Android Studio.

CurryingTheme { var currentTab by remember { mutableStateOf(0) } Scaffold( bottomBar = { BottomNavigation { BottomNavigationItem( selected = currentTab == 0, icon = { Icon(Icons.Filled.Person, null) }, onClick = { currentTab = 0 } ) BottomNavigationItem( selected = currentTab == 1, icon = { Icon(Icons.Filled.Phone, null) }, onClick = { currentTab = 1 } ) } } ) { Greeting("Android") } }
Подправим Greeting для переключения текста в зависимости от состояния BottomNavigation.
when (currentTab) { 0 -> Greeting("Selected - Person") 1 -> Greeting("Selected - Phone") }
Это работает, но подобный подход чреват багами: использование случайных констант принесёт путаницу в них. Давайте заменим произвольные константы на sealed class, который поможет нам избежать возможных проблем.
sealed class BottomNavigationTab { object Person : BottomNavigationTab() object Phone : BottomNavigationTab() }
Выглядит намного надёжней. Добавим ещё небольшую фичу — вывод Snackbar при переключении. В данном случае он хорошо подчёркивает важность упрощения onClick и карринга в дальнейшем.
CurryingTheme { val scope = rememberCoroutineScope() val scaffoldState: ScaffoldState = rememberScaffoldState() var currentTab: BottomNavigationTab by remember { mutableStateOf(BottomNavigationTab.Person) } Scaffold( scaffoldState = scaffoldState, bottomBar = { BottomNavigation { BottomNavigationItem( selected = currentTab == BottomNavigationTab.Person, icon = { Icon(Icons.Filled.Person, null) }, onClick = { currentTab = BottomNavigationTab.Person scope.launch { scaffoldState.snackbarHostState.showSnackbar("Selected - ${currentTab::class.simpleName}") } } ) BottomNavigationItem( selected = currentTab == BottomNavigationTab.Phone, icon = { Icon(Icons.Filled.Phone, null) }, onClick = { currentTab = BottomNavigationTab.Phone scope.launch { scaffoldState.snackbarHostState.showSnackbar("Selected - ${currentTab::class.simpleName}") } } ) } } ) { when (currentTab) { BottomNavigationTab.Person -> Greeting("Selected - Person") BottomNavigationTab.Phone -> Greeting("Selected - Phone") } } }
Допустим, BottomNavigationItem будет далеко не один. onClick выглядит удручающе — много дублирующего кода. Это можно легко подправить, вынеся код во вложенную функцию.
fun onClick(tab: BottomNavigationTab) { currentTab = tab scope.launch { scaffoldState.snackbarHostState.showSnackbar("Selected - ${currentTab::class.simpleName}") } }
С применением этой функции onClick будет выглядеть следующим образом:
BottomNavigationItem( selected = currentTab == BottomNavigationTab.Person, icon = { Icon(Icons.Filled.Person, null) }, onClick = { onClick(BottomNavigationTab.Person) } ) BottomNavigationItem( selected = currentTab == BottomNavigationTab.Phone, icon = { Icon(Icons.Filled.Phone, null) }, onClick = { onClick(BottomNavigationTab.Phone) } )
Это рабочий вариант, но карринг с помощью частичного применения поможет ещё больше упростить onClick.
// каррированая функция fun onClick(tab: BottomNavigationTab): () -> Unit = { currentTab = tab scope.launch { scaffoldState.snackbarHostState.showSnackbar("Selected - ${currentTab::class.simpleName}") } }
// частичное применение BottomNavigationItem( selected = currentTab == BottomNavigationTab.Person, icon = { Icon(Icons.Filled.Person, null) }, onClick = onClick(BottomNavigationTab.Person) ) BottomNavigationItem( selected = currentTab == BottomNavigationTab.Phone, icon = { Icon(Icons.Filled.Phone, null) }, onClick = onClick(BottomNavigationTab.Phone) )
Мы рассмотрели один из примеров применения каррированной функции в Jetpack Compose, но цели данного преобразования значительно шире.
Каррирование широко используется в языках программирования, поддерживающих функциональную парадигму. Все языки, поддерживающие замыкание, позволяют записывать каррированные функции: например, JavaScript, C#, Kotlin, Haskell. Имеет смысл освоить этот приём, чтобы минимизировать баги в коде, упростить его для понимания, сократить количество строк, обогатить ООП-код. Удачи!
Проект доступен на GitHub
Больше полезного про Android — в нашем телеграм-канале Surf Android Team. ��десь мы публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!
