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