Closures (Re: функциональное программирование на Python)
motusОпубликовано 21.01.2006 в Статьи
Началось все с того, что Макс привел такой пример замыкания в питоне:
>>> def shamba(n): ... def f(): ... print n ... return f ... >>> g = shamba(42) >>> g() 42
Все хорошо до тех пор, пока мы не пытаемся менять значения переменных внутри замыкания:
>>> def outer(x): ... acc = 0 ... def inner(y): ... acc += y ... return acc ... map(inner, x) ... return acc ... >>> outer([1,2,3,4]) Traceback (most recent call last): File "", line 1, in ? File "", line 6, in outer File "", line 4, in inner UnboundLocalError: local variable 'acc' referenced before assignment
Проблема в том, что если слева от оператора ‘=’ находится идентификатор, питон выполняет одновременно и присваивание, и связывание – в локальном контексте. Грабли обходятся достаточно просто – нужно, чтобы в левой части был не идентификатор, а ссылка на объект, который можно поменять – например, элемент внутри контейнера:
>>> def outer(x): ... acc = [0] ... def inner(y): ... acc[0] += y ... return acc[0] ... map(inner, x) ... return acc[0] ... >>> outer([1,2,3,4]) 10
Не очень-то красиво, но работает.
После подобных упражнений мне стало любопытно, как такие трюки проходят в других языках. В ruby вылезли те же проблемы:
acc = 10000 def outer(arr) acc = 0 def inner(x) acc += x end arr.each(&method(:inner)) acc end p outer([1,2,3,4])
При вызове outer() валится с ошибкой:
NoMethodError: undefined method `+' for nil:NilClass
Попытки использовать ссылку на элемент массива, как в питоне, не помогают. Напрашивается неутешительный вывод о том, что внутренние функции в ruby не являются честными замыканиями (или это я чего-то не понимаю?)
Справедливости ради надо сказать, что с блоками в ruby дело обстоит немного лучше:
acc = 10000 # Not visible from inside the block
def outer(x)
acc = 0 # Block fails if we comment out this line
x.each { |x| acc += x }
acc
end
p outer([1,2,3,4]), acc
печатает, как и положено,
10 10000
Обратите внимание на комментарии в коде – блок связывается только с локальным контекстом, т.е. поиска вверх не происходит.
Теперь perl. Тут неподготовленного меня сразу поджидали грабли с именоваными sub-ами:
#!/usr/bin/perl -w
use strict;
my $acc = 10000;
sub outer {
my $acc = shift;
sub inner {
$acc += $_;
return $_;
}
map( &inner, @_ );
return $acc;
}
print "It kinda works: ", outer(0,1,2,3,4), " $accn";
print "But not quite: ", outer(100,1,2,3,4), " $accn";
Для именованого sub-a cвязывание происходит раз, при первом вызове outer(), т.е. программа напечатает такое:
Variable "$acc" will not stay shared at ./closure3.pl line 10. It kinda works: 10 10000 But not quite: 100 10000
Вот вам и польза от -w. Без этого ключика все работает молча и неправильно. Брр.
perldoc, разумеется, утверждает, что такое поведение не глюк, а фича, и предлагает использовать анонимные sub-ы:
#!/usr/bin/perl -w
use strict;
my $acc = 10000;
sub outer {
my $acc = shift;
my $inner = sub {
$acc += $_;
return $_;
};
map( &$inner, @_ );
return $acc;
}
print outer(0,1,2,3,4), " $accn";
print outer(100,1,2,3,4), " $accn";
тут все работает просто замечательно:
10 10000 110 10000
Вывод – идея my() в prel-e очень даже помогает жить. Мне даже кажется, что питону и ruby не хватает чего-то подобного. В конце концов, my() – это почти как let в scheme. И, кстати, надо бы на досуге поиграть с замыканиями в JavaScript-e – наличие var там сулит определенные надежды. Ну, и раз я уже упомянул scheme, вот вам напоследок пример совсем правильного замыкания – для полноты картины:
(define acc 10000) (define (outer start x) (let ((acc start)) (define (inner y) (set! acc (+ acc y)) ) (map inner x) acc ) ) (write (list (outer 0 '(1 2 3 4)) (outer 100 '(1 2 3 4)) acc )) (newline)
печатает
(10 110 10000)
Просто глаз радуется!
Так что остальным языкам еще есть куда стремиться, имно.
Понравилась статья? Подпишись на обновления по RSS/E-mail

Насчет остальных языков. Насколько я понимаю, в Java то же самое реализуется через анонимный класс и интерфейс?
скорее нет, чем да. из анонимного класса все переменные вне его доступны только по чтению. более того, они все должны быть явно обозначены
final.вот жалкая попытка функционального программирования на java:
package test.closure; import java.util.Collection; import java.util.ArrayList; public class Test { public interface Functor<T, V> { public T apply( V value ); } public static <T, V> void map( Functor<T, V> func, Collection<V> src, Collection<T> dst ) { for ( V elem : src ) { dst.add( func.apply(elem) ); } } public static void main( final String[] args ) { ArrayList<String> list = new ArrayList<String>(args.length); ArrayList<String> res = new ArrayList<String>(args.length); for ( String arg : args ) { list.add( arg ); } map( new Functor<String, String>() { private int count = 0; public String apply( String value ) { return ++count + " of " + args.length + ": " + value; } }, list, res ); System.out.println( res ); } }если попытаться вынести
counterза пределы вызоваmap()или убратьfinalпередargs, ничего не скомпилится.Немного не так. Из анонимного класса доступны только для чтения не сами переменные, а ссылки на них. Т.е. проблема решается или как в примере с Python в приведенной статье или посредством работы только с объектами.
да, верно. т.е. фактически нужно ручками пометить как final те ссылки, которые могут войти в замыкание.
У Smalltalk-а с замыканиями порядок:
Collection>>sum: initial
| acc |
acc := initial.
self do: [:e | acc := acc + e].
^acc
#(1 2 3 4 ) sum: 0 получаем 10
#(1 2 3 4) sum: 100 получаем 110
Это вообщем-то не фукциональный стиль(как и примеры выше на других языках). На Scheme в функциональном стиле могло бы выглядеть так:
(foldl + 100 ‘(1 2 3 4)) вернет 110
Вроде как нечестно использовать библиотечную функцию foldl, поэтому напишем свой fold:
(define (myfold f initial list)
(if (empty? list)
initial
(f
initial
(myfold f (car list) (cdr list)))))
Получилось не намного больше реализации с set! и встроенной функцией “+”, зато мы получили возможность выполнять не только сложение:
(myfold + 100 ‘(1 2 3 4)) вернет 110
(myfold * 2 ‘(3 5)) вернет 30
(myfold max 0 ‘(1 2 3 4)) вернет 4
У Smalltalk-а с замыканиями порядок:
Collection>>sum: initial
| acc |
acc := initial.
self do: [:e | acc := acc + e].
^acc
#(1 2 3 4 ) sum: 0 получаем 10
#(1 2 3 4) sum: 100 получаем 110
Это вообщем-то не фукциональный стиль(как и примеры выше на других языках). На Scheme в функциональном стиле могло бы выглядеть так:
(foldl + 100 '(1 2 3 4)) вернет 110
Вроде как нечестно использовать библиотечную функцию foldl, поэтому напишем свой fold:
(define (myfold f initial list)
(if (empty? list)
initial
(f
initial
(myfold f (car list) (cdr list)))))
Получилось не намного больше реализации с set! и встроенной функцией “+”, зато мы получили возможность выполнять не только сложение:
(myfold + 100 '(1 2 3 4)) вернет 110
(myfold * 2 '(3 5)) вернет 30
(myfold max 0 '(1 2 3 4)) вернет 4
Какой тэг использовать для кода?