С момента основания рубрики Практически Groovy уже почти год назад, я предоставил вам несколько возможностей познакомиться с замыканиями. Когда я первый раз написал о Groovy в рамках alt.lang.jre ("Feeling Groovy", август 2004 г.), я познакомил вас с синтаксисом замыканий Groovy, а в выпуске
последнего месяцая показал, как изменился этот синтаксис при недавних обновлениях, совместимых с JSR. Из пройденных вами материалов вы знаете, что замыкания в Groovy представляют собой блоки кода, на которые можно ссылаться, которым можно задавать параметры и которые могут быть переданы в качестве параметра метода и возвращены в качестве значения вызовом метода. Что более важно - они также могут быть параметрами и возвращаемыми значениями других замыканий. Поскольку замыкания - это объекты типа Closure, они также могут быть свойствами класса и элементами коллекции.
Хотя и всё это нельзя назвать совсем уж пресным на самом деле, методики замыканий (наверное, я должен был бы сказать рецепты), с которыми вы познакомитесь в этом месяце, будут определенно поострее, чем все, что вы пробовали до этого. Приглашенные авторы Джон Севедж и Кен Баркли провели несколько любопытных экспериментов с методом curry() в замыканиях Groovy, и в этом месяце вам посчастливилось попробовать, что же они приготовили.
Замыкания, приправленные каррингом от Баркли и Севеджа, не только пробудят аппетит к использованию знакомых операций, таких как композиции и шаблоны разработки Visitor, они также откроют двери в функциональное программирование в Groovy. Представьте себе, что вы готовите рагу с Groovy.
Если блюда с карри можно найти обычно в хороших индийских ресторанах,
функции карринга чаще встречаются в языках функционального программирования, к которым относятся ML и Haskell (см. раздел Ресурсы). Понятие карринг берет свои истоки от Хаскелла Карри (Haskell Curry), математика, разработавшего понятие частичных функций. Понятие карринга означает передачу нескольких аргументов в функцию, которая принимает множество аргументов, и возвращает функцию, которая принимает оставшиеся аргументы и возвращает результат. Отличная новость - в текущей версии Groovy (на момент написания статьи это была версия jsr-02) поддерживается метод curry() для объектов Closure, а это значит, что мы, жители Планеты Groovy, теперь можем воспользоваться преимуществами некоторых аспектов функционального программирования!
Возможно, до этого вы никогда не использовали карринг(curry()), поэтому мы начнем с самых основ. В листинге 1 показано замыкание, обозначенное как multiply. У него есть формальные параметры x и y
, а возвращает оно произведение этих двух значений. После этого код предоставляет два метода для вызова замыкания multiply: прямой (посредством call) и косвенный, при условии отсутствия неопределенности. Последний стиль приводит нас к понятию функционального вызова.
Листинг 1. Просто замыкание
def multiply = { x, y -> return x * y } // замыкание
def p = multiply.call(3, 4) // прямоый вызов
def q = multiply(4, 5) // косвенный вызов
println "p: ${p}" // p = 12
println "q: ${q}" // q = 20
|
Это замыкание всем хорошо, но мы собираемся приправить его каррингом. При вызове метода curry()
не обязательно указывать весь список фактических параметров. Вызов с
каррингом порождает создание частичного применения замыкания. Частичное применение замыкания - это еще один объект Closure, в котором зафиксированы некоторые значения.
В листинге 2 показано применение карринга к замыканию multiply. В первом примере было установлено значение параметра x, равное 3. Фактически у замыкания, обозначенного triple, теперь имеется определение вида triple = { y -> return 3 * y }.
Листинг 2. Замыкания с карри
def multiply = { x, y -> return x * y } // замыкание
def triple = multiply.curry(3) // triple = { y -> вовзрат 3 * y }
def quadruple = multiply.curry(4)
// quadruple = { y -> return 4 * y }
def p = triple.call(4) // прямой вызов
def q = quadruple(5) // косвенный вызов
println "p: ${p}" // p = 12
println "q: ${q}" // q = 20
|
Как вы можете видеть, из определения multiply был удален параметр x, а все его упоминания были заменены значением 3.
Как вы знаете из основ математики, оператор умножения является перестановочным (другими словами, x * y = y * x). С другой стороны, оператор вычитания перестановочным не является; поэтому для обработки уменьшаемого и вычитаемого нужны две операции. В листинге 3 для этой цели определены замыкания lSubtract и rSubtract (для левой и правой части соответственно), а после этого показано интересное применение функции curry.
Listing 3. Left and right operands
def lSubtract = { x, y -> return x - y }
def rSubtract = { y, x -> return x - y }
def dec = rSubtract.curry(1)
// dec = { x -> return x - 1 }
def cent = lSubtract.curry(100)
// cent = { y -> return 100 - y }
def p = dec.call(5) // прямой вызов
def q = cent(25) // косвенный вызов
println "p: ${p}" // p = 4
println "q: ${q}" // q = 75
|
Из предыдущих статей этой серии вы помните, что замыкания обычно используются с методами итераторов, применяемыми к коллекциям List и Map
. Метод итератора collect, например, применяет замыкание к каждому элементу коллекции и возвращает новую коллекцию с новыми значениями. В листинге 4 показано применение метода
collect к List и Map. Коллекция
List, обозначенная как ages, направляется в метод collect()
вместе с единственным замыканием { element -> return
element + 1 } в качестве параметра. Обратите внимание, что если последним параметром метода является замыкание, Groovy позволяет вам исключить его из списка фактических параметров и разместить сразу после закрывающей скобки. Если фактических аргументов нет, круглые скобки можно опустить. Это продемонстрировано на примере метода collect(), который вызывается с объектом Map, обозначенным как accounts.
Листинг 4. Замыкания и коллекции
def ages = [20, 30, 40]
def accounts = ['ABC123' : 200, 'DEF456' : 300, 'GHI789' : 400]
def ages1 = ages.collect({ element -> return element + 1 })
def accounts1 = accounts.collect
{ entry -> entry.value += 10; return entry }
println "ages1: ${ages1}"
// ages1: [21, 31, 41]
println "accounts1: ${accounts1}"
// accounts1: [ABC123=210, GHI789=410, DEF456=310]
def ages2 = ages.collect { element -> return dec(element) }
println "ages2: ${ages2}"
// ages2: [19, 29, 39]
|
В последнем примере собираются все элементы из List, обозначенного ages
, и к ним применяется замыкание dec (из листинга 3).
Вероятно, одной из наиболее важных черт замыканий является композиция, в которой вы можете определить замыкание, целью которого будет сочетание других замыканий. С помощью композиции можно объединить несколько простых замыканий в более сложное.
В листинге 5 показан изящное замыкание composition
. Теперь сосредоточьтесь и прочитайте внимательно: Параметры f и g представляют
замыкания с одним параметром. Пока все хорошо? Далее, к некоторому значению параметра x применяется замыкание g и к полученному результату применяется замыкание f. Вот так так! Применяя карринг к первым двум параметрам замыкания, вы фактически можете создать новое замыкание, которое будет сочетать их действия.
Листинг 5 соединяет замыкания triple и quadruple в замыкание twelveTimes. Если применить его к фактическому параметру 3, будет возвращено значение 36.
Листинг 5. Композиция замыканий – это супер!
def multiply = { x, y -> return x * y }
// замыкание
def triple = multiply.curry(3)
// triple = { y -> return 3 * y }
def quadruple = multiply.curry(4)
// quadruple = { y -> return 4 * y }
def composition = { f, g, x -> return f(g(x)) }
def twelveTimes = composition.curry(triple, quadruple)
def threeDozen = twelveTimes(3)
println "threeDozen: ${threeDozen}"
// threeDozen: 36
|
Здорово, правда?
Давайте теперь рассмотрим некоторые приправы к замыканиям. Мы начнем с механизма, с помощью которого можно выразить замыкания, реализующие шаблон вычислений - понятие из области функционального программирования. Примером шаблона вычислений может служить ситуация, в которой вам необходимо преобразовать каждый элемент List одинаковым образом. Поскольку такие шаблоны встречаются очень часто, мы разработали класс, названный Functor, который будет инкапсулировать их как static Closure. В листинге 6 показаны фрагменты кода.
Листинг 6. Функциональный элемент, инкапсулирующий шаблон вычислений
package fp
abstract class Functor {
// arithmetic (binary, left commute and right commute)
public static Closure bMultiply = { x, y -> return x * y }
public static Closure rMultiply = { y, x -> return x * y }
public static Closure lMultiply = { x, y -> return x * y }
// ...
// композиция
public static Closure composition = { f, g, x -> return f(g(x)) }
// списки
public static Closure map =
{ action, list -> return list.collect(action) }
public static Closure apply = { action, list -> list.each(action) }
public static Closure forAll = { predicate, list ->
for(element in list) {
if(predicate(element) == false) {
return false
}
}
return true
}
// ...
} |
Здесь вы можете увидеть замыкание под названием map, которое не следует путать с интерфейсом Map. У замыкания map
есть параметр f, который представляет замыкание, и параметр list, который представляет, как ни удивительно, List. Он возвращает новый List, в котором f накладывается на каждый элемент list. Конечно же, в Groovy уже есть метод collect() для
Lists, поэтому мы будем использовать его в нашей программе.
В листинге 7 мы пойдем еще дальше, применив карринг к замыканию map, что даст нам блок, который будет умножать все элементы заданного списка на 12.
Листинг 7. Добавим немного карри и умножим на 12
import fp.*
def twelveTimes = { x -> return 12 * x }
def twelveTimesAll = Functor.map.curry(twelveTimes)
def table = twelveTimesAll([1, 2, 3, 4])
println "table: ${table}"
// table: [12, 24, 36, 48]
|
Это именно то, что мы называем пятизвездочными вычислениями!
Бизнес-правила под соусом карри
Все эти кулинарные разговоры о кухне замыканий – это хорошо, но более бизнес-ориентированным читателям понравится следующий пример. Рассмотрим задачу расчета "чистой" цены определенной позиции Book, учитывая скидку магазина и различные государственные налоги, например, налог на добавленную стоимость. Если вы включите эту логику в состав класса Book, получившееся решение, вероятно, будет жестким. Если, например, магазин может изменить значение скидки, или захотеть применять ее только к некоторым позициям, то такое решение будет недостаточно гибким.
И как же быть? С помощью замыканий и карринга можно без труда приспособиться к изменению бизнес-правил. Вы можете использовать набор простых замыканий, представляющих отдельные бизнес-правила, и комбинировать их различными способами с помощью композиций. И, наконец, вы можете сопоставлять их с коллекциями с помощью шаблонов вычислений.
В листинге 8 показан пример книжного магазина. Замыкание rMultiply представляет собой частичное приложение, которое преобразует бинарное умножение в унарное замыкание, используя постоянный второй операнд. Два замыкания calcDiscountedPrice и calcTax являются экземплярами замыкания rMultiply с фиксированными значениями множителя. Замыкание calcNetPrice представляет собой алгоритм расчета "чистой" цены, сначала умножающий цену на скидку, а затем - на налог на с продаж. И, наконец, к цене книги применяется calcNetPrice.
Листинг 8. Бизнес-объект книги
import fp.*
class Book {
@Property name
@Property author
@Property price
@Property category
}
def bk = new Book(name : 'Groovy', author :
'KenB', price : 25, category : 'CompSci')
// постоянные
def discountRate = 0.1
def taxRate = 0.17
// замыкание книги
def calcDiscountedPrice = Functor.rMultiply.curry(1 - discountRate)
def calcTax = Functor.rMultiply.curry(1 + taxRate)
def calcNetPrice =
Functor.composition.curry(calcTax, calcDiscountedPrice)
// расчет чистой цены
def netPrice = calcNetPrice(bk.price)
println "netPrice: ${netPrice}" // netPrice: 26.325
|
Вы увидели, как можно применить замыкания и карринг к функциональным шаблонам, теперь давайте посмотрим, что будет, если мы применим такие же методики к важному шаблону объектно-ориентированного проектирования. В обычном сценарии использования объектно-ориентированная система должна обходить набор объектов и выполнять с каждым из его элементов определенные действия. В другом сценарии система будет обходить тот же набор, но выполнять другие действия. Обычно это можно сделать с помощью шаблонов разработки Visitor (см. раздел
Ресурсы). Интерфейс Visitor предоставляет протокол действия для обработки элемента коллекции. Отдельные подклассы определяют различные необходимые режимы работы. После этого вводится метод, который обходит коллекцию и применяет действие Visitor к каждому элементу.
Если вы еще не догадались, для достижения такого же результата можно использовать замыкания. Одна из привлекательных особенностей такого подхода состоит в том, что при использовании замыканий вам не нужно разрабатывать иерархию класса visitor. Более того, вы можете эффективно использовать композицию и наложение замыканий для определения действий и организации обхода коллекции!
Например, рассмотрим отношение "один ко многим" между классом Library и классом Book, которые используются для инвентаризации запасов библиотеки. Для реализации такого отношения можно использовать List или Map; однако у
Map есть преимущество в том, что она обеспечивает быстрый поиск, скажем, по заданному в качестве ключа номеру книги по каталогу.
В листинге 9 показано простое отношение "один ко многим", использующее Map. Обратите внимание на два метода "display" класса Library. Введение visitor делает оба этих метода кандидатами на реорганизацию с использованием visitor.
Листинг 9. Приложение библиотеки
class Book {
@Property title
@Property author
@Property catalogNumber
@Property onLoan = false
String toString() {
return "Title: ${title}; author: ${author}"
}
}
class Library {
@Property name
@Property stock = [ : ]
def addBook(title, author, catalogNumber) {
def bk = new Book(title : title, author :
author, catalogNumber : catalogNumber)
stock[catalogNumber] = bk
}
def lendBook(catalogNumber) {
stock[catalogNumber].onLoan = true
}
def displayBooksOnLoan() {
println "Library: ${name}"
println "Books on loan"
stock.each { entry ->
if(entry.value.onLoan == true) println entry.value
}
}
def displayBooksAvailableForLoan() {
println "Library: ${name}"
println "Books available for loan"
stock.each { entry ->
if(entry.value.onLoan == false) println entry.value
}
}
}
def lib = new Library(name : 'Napier')
lib.addBook('Groovy', 'KenB',
'CS123')
lib.addBook('Java', 'JohnS', 'CS456')
lib.addBook('UML', 'Ken and John',
'CS789')
lib.lendBook('CS123')
lib.displayBooksOnLoan() // Title: Groovy; author: KenB
lib.displayBooksAvailableForLoan() // Title: UML; author: Ken and John
// Title: Java; author: JohnS
|
В класс Library, который имитирует использование visitor, показанный в листинге 10, включены несколько замыканий. Замыкание
action (чем-то похожее на замыкание map) применяет замыкание action к каждому элементу List. Замыкание displayLoanedBook отображает книгу в том случае, если она выдана читателю, а замыкание displayAvailableBook выводит книгу, если она в хранилище. Оба действуют как visitor и связанные действия. Применение карринга к замыканию apply с результатом displayLoanedBook в замыкании displayLoanedBooks, подготовленным для обработки коллекции книг. Похожая схема используется для обработки вывода книг, доступных для выдачи, как показано в листинге 10.
Листинг 10. Исправленный visitor библиотеки
import fp.*
class Book {
@Property title
@Property author
@Property catalogNumber
@Property onLoan = false
String toString() {
return " Title: ${title}; author: ${author}"
}
}
class Library {
@Property name
@Property stock = [ : ]
def addBook(title, author, catalogNumber) {
def bk = new Book(title : title, author :
author, catalogNumber : catalogNumber)
stock[catalogNumber] = bk
}
def lendBook(catalogNumber) {
stock[catalogNumber].onLoan = true
}
def displayBooksOnLoan() {
println "Library: ${name}"
println "Books on loan"
displayLoanedBooks(stock.values())
}
def displayBooksAvailableForLoan() {
println "Library: ${name}"
println "Books available for loan"
displayAvailableBooks(stock.values())
}
private displayLoanedBook = { bk -> if(bk.onLoan == true)
println bk }
private displayAvailableBook = { bk -> if(bk.onLoan == false)
println bk }
private displayLoanedBooks =
Functor.apply.curry(displayLoanedBook)
private displayAvailableBooks =
Functor.apply.curry(displayAvailableBook)
}
def lib = new Library(name : 'Napier')
lib.addBook('Groovy', 'KenB',
'CS123')
lib.addBook('Java', 'JohnS', 'CS456')
lib.addBook('UML', 'Ken and John',
'CS789')
lib.lendBook('CS123')
lib.displayBooksOnLoan() // Title: Groovy; author: KenB
lib.displayBooksAvailableForLoan() // Title: UML; author: Ken and John
// Title: Java; author: JohnS
|
Тестирование с помощью замыканий
Перед тем, как подводить итоги, давайте рассмотрим еще одно применение замыканий Groovy. Рассмотрим приложение, которое моделирует компанию (Company) с множеством работников (Employee). Рекурсивное отношение организует агрегацию "один ко многим" между одним Employee (руководитель группы) и многими Employee (члены группы). На рисунке 1 показана схема класса для такой организации.
Рисунок 1. Приложение Company
Вы можете использовать замыкания для оценки архитектурной целостности ваших моделей. Например, в рассматриваемом примере вы можете удостовериться, что каждому работнику назначен менеджер. Простое замыкание hasManager выражает это требование для отдельно взятого сотрудника: def hasManager = { employee -> return (employee.manager != null) }.
Частичное приложение замыкания forAll
из приведенного выше, в листинге 6, класса Functor позволяет описать это условие как архитектурное требование: def
everyEmployeeHasManager = Functor.forAll.curry(hasManager).
В листинге 11 показано приложение с замыканиями и каррингом для проверки архитектурной целостности системы.
Листинг 11. Применение замыканий для проверки архитектурной целостности
import fp.*
/**
* Компания с несколькими сотрудниками.
* Каждый работник отвечает перед руководителем группы,
* который, в свою очередь, управляет командой сотрудников.
*/
import java.util.*
class Employee {
@Property id
@Property name
@Property staff = [ : ]
@Property manager = null
String toString() {
return "Employee: ${id} ${name}"
}
def addToTeam(employee) {
staff[employee.id] = employee
employee.manager = this
}
}
class Company {
@Property name
@Property employees = [ : ]
def hireEmployee(employee) {
employees[employee.id] = employee
}
def displayStaff() {
println "Company: ${name}"
println "===================="
employees.each { entry -> println "
${entry.value}" }
}
}
def co = new Company(name : 'Napier')
def emp1 = new Employee(id : 123, name : 'KenB')
def emp2 = new Employee(id : 456, name : 'JohnS')
def emp3 = new Employee(id : 789, name : 'JonK')
co.hireEmployee(emp1)
co.hireEmployee(emp2)
co.hireEmployee(emp3)
emp3.addToTeam(emp1)
emp3.addToTeam(emp2)
co.displayStaff()
// Архитектурные замыкания
def hasManager = { employee -> return (employee.manager != null) }
def everyEmployeeHasManager = Functor.forAll.curry(hasManager)
def staff = new ArrayList(co.employees.values())
println "Every employee has a manager?:
${everyEmployeeHasManager.call(staff)}" // false
|
В этом месяце вы познакомились с множеством замыканий, и я надеюсь, это было достаточно ново, чтобы поддержать ваш аппетит. Как вы узнали из примеров с умножением, замыкания с применением карринга позволяют с удивительной легкостью реализовывать функциональные шаблоны вычислений. После того, как вы научитесь работать с этими шаблонами, вы сможете внедрять их в типичные корпоративные сценарии, например, так, как мы применили их к бизнес-правилам книжного магазина. Применение замыканий к функциональным шаблонам вдохновляет, и от этого совсем недалеко до их применения к шаблонам объектно-ориентированного проектирования. Для имитации наиболее важных элементов шаблона Visitor могут использоваться замыкания с применением карринга, как мы показали в примере Library. Они также могут быть полезны для выполнения проверок на целостность при тестировании программного обеспечения, как мы показали в примере Company.
Все примеры, которые вы увидели в этом месяце, являются практическими сценариями использования для корпоративных систем. Знание того, как гибко можно применить замыкания Groovy и метод
curry к различным сценариям программирования, а также функциональным и объектно-ориентированным шаблонам, очень воодушевляет. Хотя Хаскелл Карри, несомненно, найдет это крайне groovy – скучным. !
Научиться
- Оригинал статьи Functional programming with curried closures (EN).
- "Развитие Groovy" (Эндрю Гловер, developerWorks, июль 2005 г.): рассматривает многие из изменений синтаксиса Groovy, совместимые с JSR (в том числе замыкания).
- "Функциональное программирование на языке Java" (EN) (Абхиджит Белапуркар (Abhijit Belapurkar), developerWorks, июль 2004 г.): учебник по использованию конструкций функционального программирования, таких как замыкания и функции высшего порядка, в языке Java.
- "Знакомство с Haskell" (EN) (Дэвид Мертц, developerWorks, сентябрь 2001 г.): учебник для тех, кто хочет узнать больше о функциональном программировании.
-
Practically Groovy
: познакомьтесь со всеми статьями этой серии, которые опираются друг на друга с развитием серии.
-
ML для работающего программиста
(EN) (Лоренс С. Полсон (Lawrence C. Paulson), издательство Cambridge University Press, 1996 г.): хорошее введение в этот язык функционального программирования.
-
Haskell: Ремесло функционального программирования
(EN) (Саймон Томпсон, издательство Addison Wesley, 1999 г.): прекрасный способ изучить Haskell.
-
Шаблоны проектирования: Начала объектно-ориентированного программного обеспечения многократного использования
(EN) (Гамма (Gamma) и др., издательство Addison Wesley, 1995 г.): узнайте больше о шаблонах объектно-ориентированного проектирования, речь о которых шла в этой статье.
- Раздел Технология Java на developerWorks: множество статей по всем аспектам программирования на Java.
Получить продукты и технологии
-
Страница проекта Groovy: загрузите Groovy и узнайте больше о замыканиях и их композиции.
Обсудить
Приглашенный автор Кен Баркли (Ken Barclay) преподает на факультете компьютерных технологий Университета Нейпир в Эдинбурге, Шотландия. Он специализируется на объектно-ориентированном проектировании, разработке и программировании. Он является соавтором книги Объектно-ориентированное проектирование с помощью UML и Java (издательство Butterworth Heinemann, 2003).
Приглашенный автор Джон Севедж (John Savage) преподает на факультете компьютерных технологий Университета Нейпир в Эдинбурге, Шотландия. Он специализируется на объектно-ориентированном проектировании, разработке и программировании. Он является соавтором книги Объектно-ориентированное проектирование с помощью UML и Java (издательство Butterworth Heinemann, 2003).
Эндрю Гловер является президентом компании Stelligent Incorporated , которая помогает другим фирмам решать проблемы качества программного обеспечения. Для этого используются эффективные стратегии тестирования и технологии непрерывной интеграции, которые позволяют коллективам разработчиков постоянно контролировать качество кода, начиная с ранних стадий разработки. Просмотрите блог Энди , там можно найти список его публикаций.