Как стать автором
Обновить
70.55
Surf
Создаём веб- и мобильные приложения

Функциональный подход в Jetpack Compose: каррирование функций

Время на прочтение5 мин
Количество просмотров3.6K

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.

Demo currying app
Demo currying app
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

Кейсы Surf на сайте

Больше полезного про Android — в нашем телеграм-канале Surf Android Team. Здесь мы публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии6

Публикации

Информация

Сайт
surf.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия