Пересекая границы: Замыкания

Больше, чем просто лестные слова

Замыкания (closures) представляют собой фрагменты (блоки) кода, который можно использовать в качестве аргументов функций и методов. Эта программная конструкция уже давно является основным элементом таких языков как Lisp, Smalltalk и Haskell. До сих пор Java™-сообщество сопротивлялось внедрению замыканий, даже несмотря на то, что они были добавлены в конкурирующие языки, например, C#. В данной статье исследуется вопрос, действительно ли замыкания вводят в язык излишнюю сложность ради незначительного удобства, или это нечто большее.

Брюс Тэйт, президент, RapidRed

Брюс Тэйт (Bruce Tate) является отцом, горным байкером и байдарочником, проживающим в Austin, Texas. Он автор трех бестселлеров по языку Java, в том числе, победителя Jolt "Лучше, быстрее, легче Java". Недавно издал "За пределами Java"… Он работал 13 лет в IBM, а сейчас является основателем RapidRed consultancy, где специализируется на стратегиях и архитектурах облегченной разработки, основанных на технологии Java и Ruby on Rails.



06.03.2007

Когда лет 10 назад я стал байкером, то начал с самого простого велосипеда с минимально возможным числом компонентов. Позднее я понял, что некоторые компоненты, например, задний амортизатор, защищают мою спину и сам велосипед в горах Техаса. Я стал ездить быстрее и меньше падать. Затраты на усложнение и техническое обслуживание оправдали себя.

О данной серии статей

В серии статей "Пересекая границы" ее автор Брюс Тэйт развивает мнение о том, что современные Java-программисты имеют широкие возможности для изучения других подходов и языков. Программирование очень изменилось после того, как Java-технология стала очевидным лучшим выбором для всех разрабатываемых проектов. Другие интегрированные среды формируют способ построения Java-сред, а концепции, присущие другим языкам, могут разнообразить Java-программирование. Написанный Python-код (или Ruby, или Smalltalk, или ... замените многоточие) может изменить способ Java-кодирования.

Данная серия статей знакомит с концепциями и методиками программирования, которые радикально отличны от Java-разработки, но в то же время напрямую применимы к ней. В одних случаях необходимо интегрировать технологию для использования ее возможностей. В других можно применить концепции непосредственно. Конкретное инструментальное средство не так важно, как идея о том, что другие языки программирования и интегрированные среды могут оказать влияние на разработчиков, среды разработки и даже на фундаментальные подходы в Java-сообществе.

Поклонники Java в настоящее время втянуты в аналогичные дебаты относительно замыканий. Некоторые считают, что дополнительная сложность, привносимая в язык замыканиями, не оправдана. Они аргументируют это тем, что небольшое удобство не стоит той цены, которую придется заплатить за нарушение синтаксической простоты. Некоторые верят, что эта дополнительная сложность поднимет новую волну шаблонов проектирования. Для поиска наилучших ответов на вопрос необходимо пересечь границы и посмотреть, как программисты используют замыкания в других языках.

Замыкания 101 в Ruby

Замыкания являются непоименованными функциями с ограниченной областью действия. Я рассмотрю каждую из этих концепций более подробно по ходу статьи, но сначала лучше все упростить. Представляйте замыкания как блок кода, который можно использовать в качестве аргумента с особыми правилами области действия. Я буду использовать Ruby для демонстрации работы замыканий. Чтобы поработать самостоятельно, можно загрузить примеры (см. раздел "Загрузка"), а также загрузить и установить Ruby (cм. раздел "Ресурсы"). Запустите интерпретатор командой irb и загружайте каждый пример командой load filename. В листинге 1 показано самой простое замыкание:

Листинг 1. Самое простое замыкание
3.times {puts "Inside the times method."}


Results:
Inside the times method.
Inside the times method.
Inside the times method.

times - это метод объекта 3. Он выполняет код замыкания три раза. {puts "Inside the times method."} - это замыкание. Это непоименованная функция, передаваемая в метод times и выводящая статическое предложение. Этот код компактнее и проще альтернативы с циклом, показанной в листинге 2:

Листинг 2: Цикл без замыканий
for i in 1..3 
  puts "Inside the times method."
end

Первым расширением, добавляемым Ruby к простому блоку кода, является список аргументов. Метод или функция могут взаимодействовать с замыканием через аргументы. В Ruby аргументы выражаются при помощи разделенного запятыми списка аргументов внутри символов ||, например, |argument, list|. Используя аргументы таким способом, можно легко создать итерацию по таким структурам как массив. В листинге 3 приведен пример итерации по массиву в Ruby:

Листинг 3. Использование замыканий с коллекциями
['lions', 'tigers', 'bears'].each {|item| puts item}


Results: 
lions
tigers
bears

Метод each - это только один из способов итерации. Часто возникает потребность сгенерировать новую коллекцию с результатами операции. Такой метод в Ruby называется collect. Может также возникнуть потребность соединить содержимое массива с некоторой произвольной строкой. В листинге 4 приведен такой пример. Это только два из многих метода итерации с использованием замыканий.

Листинг 4. Передача аргументов в замыкание
animals = ['lions', 'tigers', 'bears'].collect {|item| item.upcase}
puts animals.join(" and ") + " oh, my."

LIONS and TIGERS and BEARS oh, my.

В листинге 4 первая строка кода берет каждый элемент массива, вызывает замыкание и генерирует коллекцию результатов. Вторая строка соединяет все элементы в выражение со словом " and " между каждым из них. До сих пор я не показал ничего, кроме синтаксической привлекательности. Все это можно сделать в любом языке.

Из этих примеров видно, что непоименованные функции - это просто функции, не имеющие имени, которые вычисляются по своему месторасположению и которые определяют свой контекст на основе этого месторасположения. Но если бы единственным различием между языками (с замыканиями и без) было небольшое синтаксическое улучшение (отсутствие необходимости объявлять функцию) - это не вызвало бы так много дискуссий. Преимущества замыканий выходят далеко за пределы экономии нескольких строк кода, а шаблоны использования - это не только простая итерация.

Вторым свойством замыканий является закрытая область действия, которую я смогу лучше продемонстрировать на еще одном примере. Допустим, имеется массив цен, и я хотел бы сгенерировать таблицу налогов на продажу с ценой и соответствующим ей налогом. Процент налога не хотелось бы программировать в замыкании жестко. Лучше указать его где-то еще. В листинге 5 показана одна из возможных реализаций:

Листинг 5. Использование замыканий для создания таблицы с налогами
tax = 0.08

prices = [4.45, 6.34, 3.78]
tax_table = prices.collect {|price| {:price => price, :tax => price * tax}}

tax_table.collect {|item| puts "Price: #{item[:price]}    Tax: #{item[:tax]}"}


Results:
Price: 4.45    Tax: 0.356
Price: 6.34    Tax: 0.5072
Price: 3.78    Tax: 0.3024

Перед рассмотрением области действия я должен объяснить пару идиом Ruby. Во-первых, символ - это идентификатор, начинающийся с двоеточия. Представляйте символ как имя некоторой абстрактной концепции. :price и :tax - это символы. Во-вторых, можно легко подставлять значения переменных внутри строки. В шестой строке используется преимущество такой методики: puts "Price: #{item[:price]} Tax: #{item[:tax]}". А теперь вернемся к теме области действия.

Взгляните на первую и четвертую строки кода в листинге 5. В первой строке переменной tax присваивается значение. В четвертой строке эта переменная используется для вычисления столбца tax таблицы price. Но она используется в замыкании, то есть этот код на самом деле выполняется в контексте метода collect! Теперь понятна суть термина замыкание. Область действия пространства имен в объявленном блоке и функции, использующей его, по существу является одной областью действия - область действия закрыта. Это важнейшее свойство. Эта закрытая область действия является средством, связывающим замыкание с вызывающей функцией и кодом, определяющим его.


Работа с замыканиями

Вы увидели, как использовать замыкания в качестве клиента. Ruby позволяет также писать методы, использующие ваши собственные замыкания. Эта свобода означает, что Ruby API может быть более компактным, поскольку не нужно будет определять в коде каждую модель использования. Если нужно, при помощи замыканий можно создавать собственные абстракции. Например, Ruby имеет ограниченный набор итераторов, но язык прекрасно обходится без них, поскольку в коде можно создать свои собственные итеративные концепции через замыкания.

Для создания функции, использующей замыкания, просто используется ключевое слово yield, активирующее замыкание. В листинге 6 приведен пример. Функция paragraph указывает первое и последнее предложение для вывода. Пользователь может предоставить дополнительные выражения при помощи замыкания.

Листинг 6. Создание методов, использующих замыкания
def paragraph 
  puts "A good paragraph should have a topic sentence."
  yield
  puts "This generic paragraph has a topic, body, and conclusion."
end

paragraph {puts "This is the body of the paragraph."}


Results:
A good paragraph should have a topic sentence.
This is the body of the paragraph.
This generic paragraph has a topic, body, and conclusion.

Преимущества

В пользовательских замыканиях легко воспользоваться преимуществами работы с аргументами, присоединяя список параметров к yield, как показано в листинге 7:

Листинг 7. Присоединение списка параметров
def paragraph
  topic =  "A good paragraph should have a topic sentence, a body, and a conclusion. "
  conclusion = "This generic paragraph has all three parts."

  puts topic 
  yield(topic, conclusion) 
  puts conclusion
end


t = ""
c = ""
paragraph do |topic, conclusion| 
  puts "This is the body of the paragraph. "
  t = topic
  c = conclusion
end

puts "The topic sentence was: '#{t}'"
puts "The conclusion was: '#{c}'"

Тем не менее, внимательно устанавливайте область действия. Объявляемые в замыкании аргументы имеют локальную область действия. Программа, показанная в листинге 7, работает, а показанная в листинге 8, - нет, поскольку переменные topic и conclusion имеют локальную область действия:

Листинг 8. Некорректная область действия
def paragraph
  topic =  "A good paragraph should have a topic sentence."      
  conclusion = "This generic paragraph has a topic, body, and conclusion."

  puts topic 
  yield(topic, conclusion) 
  puts conclusion
end


my_topic = ""
my_conclusion = ""
paragraph do |topic, conclusion|     # these are local in scope
  puts "This is the body of the paragraph. "
  my_topic = topic
  my_conclusion = conclusion
end

puts "The topic sentence was: '#{topic}'"
puts "The conclusion was: '#{conclusion}'"

Замыкания на практике

Некоторые обычные сценарии использования замыкания:

  • Рефакторинг
  • Настройка
  • Итерация по коллекциям
  • Управление ресурсами
  • Принудительное применение политики

Научившись создавать собственные замыкания простым и удобным способом, вы обнаружите технические приемы, открывающие абсолютно новые возможности. Рефакторинг преобразовывает рабочий код в более хороший рабочий код. Большинство Java-программистов ищут возможности для рефакторинга изнутри. Они часто ищут повторения в контексте метода или цикла. Используя замыкания, можно выполнять рефакторинг снаружи.

Работа с замыканиями может привести к неожиданностям. В листинге 9, коротком примере из Ruby on Rails, показано замыкание, использующееся для кодирования ответа на HTTP-запрос. Rails передает входящий запрос в контроллер, который должен сгенерировать ожидаемые клиентом данные (технически, контроллер интерпретирует результат, основанный на содержимом, которое установил клиент в HTTP-заголовке accept). Это легко сделать при помощи замыкания.

Листинг 9. Интерпретация HTTP-результата с использованием замыканий
@person = Person.find(id)
respond_to do |wants|
  wants.html { render :action => @show }
  wants.xml { render :xml => @person.to_xml }
end

Код в листинге 9 выглядит красиво, и с первого взгляда можно точно сказать, что он делает. Если блок request запрашивает HTML, то выполняется первое замыкание; если запрашивается XML, то выполняется второе. Также легко можно представить реализацию. wants - это оболочка HTTP-запроса. Код имеет два метода (xml и html), каждый из которых принимает замыкания. Каждый метод может избирательно вызвать свое замыкание в зависимости от содержимого заголовка accept, как показано в листинге 10:

Листинг 10. Реализация запроса
  def xml
    yield if self.accept_header == "text/xml"
  end
  
  def html
    yield if self.accept_header == "text/html"
  end

Итерация является, конечно же, наиболее обычным применением замыканий в Ruby, но этот сценарий шире использования встроенных в коллекции замыканий. Представим типы замыканий, использующихся каждый день. XML-документы - это коллекции элементов. Web-страницы - это особый случай XML. Базы данных сформированы из таблиц, которые в свою очередь созданы из строк. Файлы - это коллекции символов или байт, и, часто, текстовых строк или объектов. При помощи замыканий Ruby решает каждую из этих задач достаточно хорошо. Ранее были продемонстрированы некоторые примеры, выполняющие итерацию по коллекциям. В листинге 11 показано замыкание, которое выполняет итерацию по таблице базы данных:

Листинг 11. Итерация по строкам базы данных
require 'mysql'

db=Mysql.new("localhost", "root", "password")
db.select_db("database")

result = db.query "select * from words"
result.each {|row| do_something_with_row}

db.close

Код в листинге 11 также показывает еще один возможный шаблон использования. MySQL API требует от пользователя создать ресурс базы данных и закрыть его, используя метод close. Ruby-разработчики часто используют этот шаблон для работы с такими ресурсами как файлы. Используя Ruby API, не нужно открывать или закрывать файл или управлять исключительными ситуациями. Методы класса Files делают это за вас. Вместо этого можно использовать замыкание, как показано в листинге 12:

Листинг 12. Работа с File с использованием замыкания
File.open(name) {|file| process_file(file)}

Замыкания имеют еще одно огромное преимущество: они могут облегчить принудительное применение политик. Например, можно быть абсолютно уверенным в том, что программа всегда будет заключать код транзакции между соответствующими вызовами функций. Интегрированная среда может управлять политикой, а пользовательский код, предоставляемый в замыкании, использовать ее. В листинге 13 показан базовый шаблон использования.

Листинг 13. Принудительное применение политики
def do_transaction
   begin
      setup_transaction
      yield
      commit_transaction
   rescue
      roll_back_transaction
   end
end

Замыкания в языке Java

Язык Java формально не поддерживает замыканий сам по себе, но он позволяет имитировать их. Для реализации замыканий можно использовать непоименованные inner-классы. Интегрированная среда Spring использует эту методику во многом по тем же причинам, что и Ruby. Spring-шаблоны для персистенции разрешают выполнять итерацию по полученным наборам, не обременяя пользователя подробностями управления исключительными ситуациями, выделения ресурсов или очистки. В листинге 14 показан фрагмент из примера Spring-приложения petclinic:

Листинг 14. Inner-классы как замена замыканиям
JdbcTemplate template = new JdbcTemplate(dataSource);
final List names = new LinkedList(); 
template.query("SELECT id,name FROM types ORDER BY name",
                           new RowCallbackHandler() {
                               public void processRow(ResultSet rs) 
                                     throws SQLException
                               {
                                  names.add(rs.getString(1));
                               } 
                            });

Подумайте о вещах, которых в листинге 14 не должен делать программист:

  • Открыть соединение
  • Закрыть соединение
  • Выполнить итерацию
  • Обработать исключительные ситуации
  • Иметь дело с независимыми от базы данных задачами

Программист свободен от этих задач, поскольку ими занимается интегрированная среда. Однако непоименованные inner-классы предоставляют собой только слабое подобие замыканий, и они не идут так далеко, как хотелось бы. Взгляните на бледный синтаксис листинга 14. Как минимум половина примера - это накладные расходы. Всякий раз, когда вы используете непоименованные классы, на вас словно выливают ведро холодной воды. Все дополнительные усилия на излишний синтаксис нивелируют преимущества его использования. Рано или поздно придется бросить все это. Если конструкции языка ужасны и неповоротливы, они не находят применения. Нехватка Java-библиотек, которые эффективно используют непоименованные inner-классы, усугубляет проблему. Для того чтобы замыкания в языке Java были удобны и широко распространены, они должны быть кратки и понятны.

Прежде замыкания никогда не являлись серьезным приоритетом для Java-разработчиков. Разработчики Java не поддерживали замыкания, потому что Java-пользователи сдержанно относились к выделению памяти в куче для переменных без явного new (см. раздел "Ресурсы"). Сегодня очень много говорится о включении замыканий в основной язык. За последние годы практический интерес к таким динамическим языкам как Ruby, JavaScript, и даже Lisp привел к широкой поддержке пользователями идеи включения замыканий в язык Java. Все идет к тому, что мы, наконец, получим замыкания в Java 1.7. Хорошие вещи происходят, если вы продолжаете пересекать границы.


Загрузка

ОписаниеИмяРазмер
Пример Ruby-файлов для данной статьиj-cb01097.zip4KB

Ресурсы

Научиться

  • Оригинал статьи "Crossing borders: Closures".
  • "Замыкание" (Мартин Фаулер (Martin Fowler), martinfowler.com, сентябрь 2004): Классическая статья, описывающая замыкания для Java-программистов.
  • Re: связывание и присваивание: Гай Стил (Guy Steele) описывает некоторые из ранних рассуждений, стоящих за выбором не реализовывать замыкания в Java.
  • "Замыкания и Java: Учебное руководство" (The Fishbowl, май 2003): Данное руководство описывает способы имитации замыканий с использованием непоименованных inner-классов.
  • "Функциональное программирование в Java" (Абхижит Белапуркар (Abhijit Belapurkar), developerWorks, июль 2004): В этой статье рассмотрены способы использования замыканий и функций более высокого порядка для написания модульного Java-кода.
  • "Появятся ли замыкания в Java 1.7?" ((Дежан Босанак (Dejan Bosanac), O'Reilly Network, август 2006): Как и когда мы увидим замыкания в языке программирования Java.
  • Замыкания для Java (Джилад Браха (Gilad Bracha), Нил Гафтер (Neal Gafter), Джеймс Гослинг (James Gosling) и Питер фон дер Ахи (Peter von der Ahé)): Рекомендованная спецификация для замыканий в Java.
  • От Java к Ruby: Что должен знать каждый менеджер (Pragmatic Bookshelf, 2006): Книга автора о том, когда и где имеет смысл перейти от Java-программирования к Ruby on Rails и как это сделать.
  • За пределами Java (O'Reilly, 2005): Книга автора о подъеме и становлении языка программирования Java, о технологиях, которые могут бросить вызов Java-платформе в некоторых областях.
  • Раздел, посвященный Java-технологиям: Сотни статей по каждому аспекту Java-программирования.

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

  • Ruby: Загрузите Ruby с Web-сайта проекта.

Обсудить

Комментарии

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=200245
ArticleTitle=Пересекая границы: Замыкания
publish-date=03062007