Как стать автором
Обновить
706.34
OTUS
Цифровые навыки от ведущих экспертов

Функциональное программирование на Groovy

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров2.3K

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

Функциональное программирование подразумевает стиль кодирования, акцентирующий внимание на использовании функций и минимизации изменений состояния с помощью неизменяемых структур данных. В 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 рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.

Теги:
Хабы:
Всего голосов 8: ↑7 и ↓1+9
Комментарии4

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS