Привет, Хабр!

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

В этой статье мы разберём, как реализовано ФП в Groovy.

Основы

В функциональном программировании функции считаются объектами первого класса, т.е они могут быть присвоены переменным, переданы как аргументы другим функциям или возвращены как результат работы функции. В Groovy это реализовано через замыкания, которые можно легко передавать вокруг, как и любые другие объекты. Например, можно определить функции для вычисления квадрата и куба числа, а затем использовать другую функцию для их вызова:

def fruits = ["banana", "apple", "grape", "pear"]
def upperCaseFruits = fruits.collect { it.toUpperCase() }
println upperCaseFruits // Выведет: [BANANA, APPLE, GRAPE, PEAR]

Неизменяемость — основной принцип ФП, который помогает избежать ошибок, связанных с изменением данных. В Groovy можно использовать неизменяемые коллекции, которые гарантируют, что данные в коллекции не будут изменены после их создания. Пример создания неизменяемого списка:

def immutableList = [1, 2, 3].asImmutable()
immutableList << 4  // вызовет исключение, так как список неизменяем

Для примера создадим сервис будет принимать JSON с данными пользователя, обрабатывать его, сохранять в БД и возвращать обновленные данные в ответе:

import groovy.json.JsonSlurper
import groovy.json.JsonBuilder

class UserService {
    static def processUserRequest(requestJson) {
        def jsonSlurper = new JsonSlurper()
        def userData = jsonSlurper.parseText(requestJson)

        // предположим, что userData содержит поля: id, name, age
        def updatedUserData = userData.collectEntries {
            switch (it.key) {
                case "name":
                    [it.key, it.value.toUpperCase()]
                case "age":
                    [it.key, it.value + 1]
                default:
                    [it.key, it.value]
            }
        }

        // логика сохранения данных в базу (пример)
        def dbResult = saveToDatabase(updatedUserData)

        // возвращаем результат в формате JSON
        return new JsonBuilder(dbResult).toPrettyString()
    }

    static def saveToDatabase(userData) {
        // эмуляция сохранения в БД
        println "Saving data to the database: $userData"
        userData.age = userData.age + 10  // пример изменения данных перед сохранением
        return userData
    }
}

// пример использования
def jsonRequest = '{"id": "123", "name": "John Doe", "age": 30}'
def jsonResponse = UserService.processUserRequest(jsonRequest)
println "Response: $jsonResponse"

Высшие порядки функций и композиция функций

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

def applyToList(Closure func, List items) {
    items.collect { item -> func(item) }
}

С помощью этой функции можно легко применить любую другую функцию к списку элементов.

Композиция функций позволяет комбинировать сложные операции из более простых функций. В Groovy это можно сделать с помощью оператора композиции <<:

def addOne = { it + 1 }
def square = { it * it }
def addOneThenSquare = addOne << square

assert addOneThenSquare(4) == 25  // сначала добавляет 1, затем возводит в квадрат

Так можно строить сложные трансформации, сохраняя простоту каждой функции.

Решим бизнес-задачу — агрегацию данных о продажах по нескольким категориям:

// определяем базовые функции для работы с данными
def add = { a, b -> a + b }
def multiply = { a, b -> a * b }

// функции для вычисления скидки и налога
def applyDiscount = { amount, discount -> amount - (amount * (discount / 100)) }
def applyTax = { amount, taxRate -> amount + (amount * (taxRate / 100)) }

// композиция функций для применения скидки и налога
def priceAfterDiscountAndTax = applyTax << applyDiscount.curry(10) // предположим, что скидка 10%

// список транзакций
class Sale {
    String category
    double amount
    int quantity
}

// пример списка транзакций
def sales = [
    new Sale(category: 'Electronics', amount: 200.0, quantity: 2),
    new Sale(category: 'Clothing', amount: 50.0, quantity: 5),
    new Sale(category: 'Groceries', amount: 20.0, quantity: 10)
]

// группировка по категориям и расчет суммы с учетом количества
def totalSalesByCategory = sales.groupBy { it.category }
    .collectEntries { category, salesList ->
        def totalAmount = salesList.sum { sale -> 
            priceAfterDiscountAndTax(sale.amount, 8) * sale.quantity // предположим, что налог 8%
        }
        [(category): totalAmount]
    }

println "Total Sales by Category: $totalSalesByCategory"

Функции applyDiscount и applyTax определяют, как применять скидки и налоги к сумме. Используя Groovy-композицию (<<), мы создаем новую функцию priceAfterDiscountAndTax, которая применяет сначала скидку, а затем налог.

Юзаем .curry() для предварительного применения скидки к функции applyDiscount.

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

Гибкие коллекции и лямбда-выражения

collect используется для преобразования каждого элемента в коллекции. Например, если хочется преобразовать список фруктов в список их названий в верхнем регистре, можно сделать так:

def numbers = [1, 2, 3, 4, 5, 6]
def evenNumbers = numbers.findAll { it % 2 == 0 }
println evenNumbers // Выведет: [2, 4, 6]

Метод принимает закрытие, которое определяет, как каждый элемент должен быть преобразован.

findAll позволяет фильтровать элементы коллекции на основе условия. Например, для фильтрации четных чисел из списка:

def numbers = [1, 2, 3, 4, 5, 6]
def evenNumbers = numbers.findAll { it % 2 == 0 }
println evenNumbers // Выведет: [2, 4, 6]

Метод groupBy используется для группировки элементов коллекции по определенному критерию. Например, если есть список юзеров и вы хотите сгруппировать их по городу:

class User {
    String name
    String city
}

def users = [
    new User(name: 'Alice', city: 'London'),
    new User(name: 'Bob', city: 'New York'),
    new User(name: 'Charlie', city: 'London')
]

def usersByCity = users.groupBy { it.city }
println usersByCity['London'].collect { it.name } // Выведет: [Alice, Charlie]

Метод возвращает карту, где ключами являются значения, по которым происходит группировка, а значениями — списки элементов, которые соответствуют каждому ключу.


Больше про функциональное программирование и не только эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.

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


  1. Iselston
    22.05.2024 16:15

    Спасибо за статью. Как раз "в тему". Сейчас по работе плотно работаем с Apache NiFi и там зачастую прибегаю к вызову python (на самом деле jython) скриптов, хотя дефолтно предлагается именно groovy. Буду попробовать :)


  1. JuryPol
    22.05.2024 16:15

    Извините, но у вас в примерах работы с коллекциями есть некоторая путаница. Один пример повторен дважды, а другого нет вовсе.


  1. ivanuil
    22.05.2024 16:15

    По прочтении сложилось впечатление, что все фишки groovy и так возможны в java через лямды и стримы. Разве что более лаконично, но, наверное, это и означает «Groovy, который изначально разрабатывался как более гибкая альтернатива Java»


    1. Beholder
      22.05.2024 16:15

      Всё хорошее из Groovy (в том числе и лаконичность) ушло в Kotlin.