Closures (Re: функциональное программирование на Python)

Началось все с того, что Макс привел такой пример замыкания в питоне:

>>> 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)
Просто глаз радуется!
Так что остальным языкам еще есть куда стремиться, имно. :-)

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn



5 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Какой тэг использовать для кода?

У 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

Немного не так. Из анонимного класса доступны только для чтения не сами переменные, а ссылки на них. Т.е. проблема решается или как в примере с Python в приведенной статье или посредством работы только с объектами.

Насчет остальных языков. Насколько я понимаю, в Java то же самое реализуется через анонимный класс и интерфейс?

Підписатись на коментарі