Практически Groovy : Функциональное программирование с помощью использованием замыканий и карринга

Традиционная конструкция Groovy приходит туда, где раньше ее никогда не было

В Groovy замыкания используются практически везде, и единственная проблема с ними состоит в том, что если вы начитаете их использовать каждый день, они могут показаться вам несколько пресными. В этом месяце приглашенные авторы Кен Баркли и Джон Севедж покажут, как разнообразить стандартные рецепты замыканий, например, дополняя стандартные замыкания и шаблоны разработки Visitor щепоткой карринга. Метод curry() был придуман Хаскеллом Карри (Haskell Curry) и реализован в языке Groovy еще до выхода версий, совместимых с JSR.

Кен Баркли, преподаватель, Napier University

Приглашенный автор Кен Баркли (Ken Barclay) преподает на факультете компьютерных технологий Университета Нейпир в Эдинбурге, Шотландия. Он специализируется на объектно-ориентированном проектировании, разработке и программировании. Он является соавтором книги Объектно-ориентированное проектирование с помощью UML и Java (издательство Butterworth Heinemann, 2003).



Джон Севедж, преподаватель, Napier University

Приглашенный автор Джон Севедж (John Savage) преподает на факультете компьютерных технологий Университета Нейпир в Эдинбурге, Шотландия. Он специализируется на объектно-ориентированном проектировании, разработке и программировании. Он является соавтором книги Объектно-ориентированное проектирование с помощью UML и Java (издательство Butterworth Heinemann, 2003).



Эндрю Гловер, президент компании, Stelligent Incorporated

Эндрю Гловер является президентом компании Stelligent Incorporated , которая помогает другим фирмам решать проблемы качества программного обеспечения. Для этого используются эффективные стратегии тестирования и технологии непрерывной интеграции, которые позволяют коллективам разработчиков постоянно контролировать качество кода, начиная с ранних стадий разработки. Просмотрите блог Энди , там можно найти список его публикаций.



09.01.2008

С момента основания рубрики Практически Groovy уже почти год назад, я предоставил вам несколько возможностей познакомиться с замыканиями. Когда я первый раз написал о Groovy в рамках alt.lang.jre ("Feeling Groovy", август 2004 г.), я познакомил вас с синтаксисом замыканий Groovy, а в выпуске последнего месяцая показал, как изменился этот синтаксис при недавних обновлениях, совместимых с JSR. Из пройденных вами материалов вы знаете, что замыкания в Groovy представляют собой блоки кода, на которые можно ссылаться, которым можно задавать параметры и которые могут быть переданы в качестве параметра метода и возвращены в качестве значения вызовом метода. Что более важно - они также могут быть параметрами и возвращаемыми значениями других замыканий. Поскольку замыкания - это объекты типа Closure, они также могут быть свойствами класса и элементами коллекции.

Хотя и всё это нельзя назвать совсем уж пресным на самом деле, методики замыканий (наверное, я должен был бы сказать рецепты), с которыми вы познакомитесь в этом месяце, будут определенно поострее, чем все, что вы пробовали до этого. Приглашенные авторы Джон Севедж и Кен Баркли провели несколько любопытных экспериментов с методом curry() в замыканиях Groovy, и в этом месяце вам посчастливилось попробовать, что же они приготовили.

Об этой серии руководств

Чтобы применять какой-либо инструмент в практике разработки, необходимо знать, как им пользоваться и когда его лучше отложить в сторону. Языки сценариев могут быть чрезвычайно полезным помощником, но только если применять их правильно и в подходящих ситуациях. В связи с этим в серии статей Practically Groovy мы рассматриваем практические применения 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 в Groovy

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

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=277303
ArticleTitle=Практически Groovy : Функциональное программирование с помощью использованием замыканий и карринга
publish-date=01092008