Привет, Хабр!
Функциональное программирование подразумевает стиль кодирования, акцентирующий внимание на использовании функций и минимизации изменений состояния с помощью неизменяемых структур данных. В 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)
JuryPol
22.05.2024 16:15Извините, но у вас в примерах работы с коллекциями есть некоторая путаница. Один пример повторен дважды, а другого нет вовсе.
ivanuil
22.05.2024 16:15По прочтении сложилось впечатление, что все фишки groovy и так возможны в java через лямды и стримы. Разве что более лаконично, но, наверное, это и означает «Groovy, который изначально разрабатывался как более гибкая альтернатива Java»
Iselston
Спасибо за статью. Как раз "в тему". Сейчас по работе плотно работаем с Apache NiFi и там зачастую прибегаю к вызову python (на самом деле jython) скриптов, хотя дефолтно предлагается именно groovy. Буду попробовать :)