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

от автора

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/815701/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *