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

Комментарии (6)


  1. MentalBlood
    05.10.2021 09:27

    Для ООП-мира привычно думать, что функции могут иметь несколько аргументов. На самом деле это не так

    Таким образом, функция не может иметь несколько аргументов. По сути, аргументы — это множество

    Ну зачем же так жестко, вы же просто другое определение дали, что множество аргументов — это и есть единственный аргумент (звучит странно, не правда ли?)


    1. RuslanShirkhanov
      26.10.2021 12:22
      -1

      Думаю, автор не подменял определения, а говорил об этом же подходе, но с другой стороны. Например, у нас есть функция с сигнатурой f : {a : Type} -> (a, a) -> a. Она принимает кортеж (a, a), который является лишь одним аргументом, ведь это всего-то терм типа (a, a). И если вместо него там будет гетерогенный список или любая другая структура данных, все равно ничто не изменится. Таким образом, обычно мы используем две операции, одна из которых отображает тип (a, a) в тип a -> a, а вторая делает обратное.


  1. Beholder
    05.10.2021 12:41

    Что-то вы туману напустили, мягко говоря. Нет тут никакого каррирования. Это всего лишь "extract function".


    Каррирование — преобразование функции от многих аргументов в набор функций, каждая из которых является функцией от одного аргумента.

    Где тут оно?


    Вариант когда функция возвращает () -> Unit сомнителен, так как никакой экономии не тут добиться, а смысл кода может быть не очевиден.


    sealed class тоже ни к чему, можно обойтись просто enum.


  1. mayorovp
    05.10.2021 13:26

    Итак, теперь мы знаем, что аргумент один — массив данных (кортеж). Тогда функцию f(a, b) можно рассматривать как множество всех функций на N от множества всех функций на N. Это выглядело бы так: f(a)(b). В таком случае можно записать: f(a) = f2; f2(b) = a + b.

    Не вижу связи.


    Типы функций a -> (b -> c) и (a, b) -> c ни разу не эквивалентны. Я бы даже сказал, каррированный вид функции и функция от кортежа — в некотором смысле противоположны друг другу.


    1. MentalBlood
      05.10.2021 14:29

      Да там вообще каша какая-то


      • Сначала сказали что аргумент один — множество аргументов, т.е. (a, b), окей
      • Потом из неоткуда достали "множество всех функций от множества всех функций",
      • раздробили "единственный" аргумент и
      • сказали, что f(a, b) ~ f(a)(b) (полная чушь)

      Можно ж было написать что-то вроде f(a, b) ~ g(a)(b), где g — такая-растакая, к чему этот мудреж с множествами функций? Мозг перекашивает при чтении


  1. elzahaggard13
    05.10.2021 21:45

    Эм, а просто рефлексию применить? И в чем тут каррирование?