Python

индекс
250,80

Рекурсия с помощью Y–комбинатора

Поводом для написания этой статьи стало желание разобраться с тем, как работает Y-комбинатор.

Чтобы мозги не ржавели и работали как часы, я стараюсь пробовать новые и необычные вещи.
Интереса ради, я скомпилировал Lua 5.x под DOS, с этим никаких проблем не было, но при проверке Lua на её стандартных тестах, я обнаружил код вычисления факториала, работу которого я не понял.
Но ясно осознал, что это нечто относится к функциональному программированию.



В итоге нашёл статью в Википедии, статью про практическое использование в Ruby и статью про Y-комбинатор на Python'е, при разборе которой я, наконец-то хоть что-то понял.

Y-комбинатор позволяет превратить обычную (в том числе и анонимную) функцию в рекурсивную. Основная его ценность — теоретическая, для обоснования теорий формальных вычислений, но некоторые находят для него практическое применение (такое ощущение появилось после чтения комментариев к вышеупомянутым статьям).

Из Википедии следует, что Y-комбинатор — это частный случай функций высшего порядка, которые принимают на вход функцию f и возвращают функцию g, такую, что f(g)=g. Может это и так, но пользы для понимания это определение мне не дало[1][2].

Попробуем разобраться на классическом примере факториала как это всё работает. Использоваться будет Python, потому что с ним я больше знаком.

В процессе разбора будет использоваться вспомогательная функцию dumb, от которой мы впоследствии избавимся.
def dumb():
    pass
 


Попробуем преобразовать следующую лямбда-функцию в рекурсивную, которая вычисляет факториал.
test=lambda f:lambda n: 1 if n==0  else  n*f(n-1)
 


Мы уже сделали шаг вперёд, так как test(dumb)(0)==1.

Попробуем вызвать test(dumb)(1). Мы получим исключение, так как интерпретатор не сможет вызвать dumb с аргументом 0. Мы можем поступить хитро и сделать вызов test(test(dumb))(1). Пойдя этой дорогой, мы может даже дойти до такого вызова test(test(test(test(test(test(test(dumb)))))))(6), который успешно считает факториал до 6.

А можно ли сделать такой вызов: test(test(...))(x), где… — бесконечное количество вызовов нашей функции test?
Можно, и в этом нам поможет Y-комбинатор.
Вот одна из его форм, в которой чётко видна такая структура вызовов:
def Y(f):
    return f(lambda x: Y(f)(x))
 


И, соответственно, факториал можно определить так:
factorial=Y(test)


В рамках определения из Википедии, можно сказать и так, что мы получили ссылку на внутреннюю лямбда-функцию (это которая lambda n: ...) и вызвали test с этой ссылкой.

Ещё одну известную рекурсивную функцию можно записать так:
fibbonacci=Y(lambda f:lambda n: 1 if n==0 or n==1 else f(n-2)+f(n-1))
 


Другая форма Y-комбинатора, в которой структура вызовов эффективно скрыта:
def Y(f):
    def g(k):
        return f(lambda a: (k(k))(a))
    return g(g)
 


Практической ценности в такой реализации факториала, конечно нет, это даже медленнее обычного рекурсивного вызова. Можно пожертвовать памятью и ускорить вычисления за счёт кэширования. Для этого нам необходимо слегка модифицировать Y-комбинатор:
def Ycache(f,data):
    def _fn(x):
        if x in data:
            return data[x]
        else:
            temp=(f(lambda x: Ycache(f,data)(x))
                 )(x)
            data[x]=temp
            return temp
    return _fn
 

И функция вычисления чисел Фиббоначчи будет выглядеть так:
fibbonacci=Ycache(test,{})
 


Вычисление 200-го числа Фиббоначчи происходит практически мгновенно, в отличии от простого рекурсивного варианта.

И напоследок, немного экзотики: быстрая сортировка (quick_sort)
qsort = lambda h: lambda lst: (lst if len(lst)<=1 else ( 
                             h([item for item in lst if item<lst[0]]) + \
                               [lst[0]]*lst.count(lst[0]) + \
                             h([item for item in lst if item>lst[0]])))
 
print( Y(qsort)([1,13,2,12,3,11,4,10,5,9,6,8,7]))


_________
Текст подготовлен в ХабраРедакторе
[1]Есть мысль, что это как-то связано с тем, что у некоторого класса функций f(f(f(f(f(f(f(f(f(f(…f(x))))))))))) будет сходиться к значению y, такому что f(y)=y.
[2]В Википедии в обсуждении написали, что Y-комбинатор не обязан решать алгоритмически неразрешимые задачи, а просто обеспечивать своё поведение на некотором классе функций.
+34
27 января 2009, 10:52
46

комментарии (38)

+3
tenshi #
вот страсть у людей — решать простые вещи сложным образом…
+5
ivlis #
Это же интересно! Тем более на простых задачах учишься пониманию новых методов, которые, возможно, будут приносить плоды в более сложных задачах, которые «в лоб» уже не решить. А можеи и не будут приность. Угадать это невозможно.
0
shai_xylyd #
Дело в том, что Y-комбинатор это единственная возможность рекурсии в лямбда исчислении.
–9
tenshi #
мозгодрочерство…
0
shai_xylyd #
не мозгодрочество, а теоретические основы программирования. Часть языков ведет свое начало от машины Тьюринга, другая от лямбда исчисления Алонзо Черча.
–7
tenshi #
язык программирования предназначен для взаимодействия человека и компьютера. компьютер не является лямбда-исчислителем. человек — тем более. спрашивается: нафига нужна такая «теоретическая основа», если она чужда обоим собеседникам? абстракции ради абстракций…
0
redchrom #
Тем, что лямбда-исчисления это основа функционального программирования — высокой и замечательной абстракции.
+2
shai_xylyd #
Ну компьютер не является и вычислителем php кода тоже, ему на вход электроны подавай, а на выходе получай.
0
tenshi #
весьма характерно — человек где-то потерялся…
+7
XaocCPS #
иногда мне кажется, что Хабр уже _слишком_ ТОТ :)

за статью спасибо, хотел бы только прояснить один момент, почему происходит столь разительное отличие в вычислении чисел Фиббоначи? с чем это связано?
0
XaocCPS #
* если я правильно понял в обоих случаях в итоге происходят рекурсивные вызовы?
+2
chiaroscuro #
Во втором случае у нас кэшируются вызовы функции, поэтому перед выполнением работы мы проверяем: если для этого значения уже вычислили функцию, то просто берем результат из хэша (или массива, или еще чего).
+2
worklez #
0
zzeus #
скорее мемоизация :]
0
worklez #
Не скорее, а конкретнее. :)
«Мемоизация» — один из этапов ДП.
0
Xitsa #
Самое интересное в том, что сама функция вид не поменяла — в некотором роде, выполнено разделение между концептом и конкретными реализациями
0
shai_xylyd #
Вторая форма Y-комбинатора сломала мне мозг. С первой формой все ясно, но понять некоторые конструкции во второй не смог, например я в упор не понимаю тип сущности g, так как она применима к самой себе; есть идея только одна идея, что запись g(g) эквивалентна x->g(g(x)). Это верно?
0
Xitsa #
Здесь очень примерно происходит следующее:
Y(f)(x)=>(g(g))(x)=>[f(λ a: (k(k))(a))](x)=>
=>[f(λ a: (g(g)(g(g)))(a))](x)=>
=>[f(λ a: (f(λ a: (g(g)(g(g)))(a))))](x)
т.е. тоже получаем бесконечную развёртку.
(В скобках мог и намудрить)
+1
shai_xylyd #
Спасибо, разобрался. Меня, как программиста, который почти всегда писал на языках со статической системой типов и думал, что динамическая система типов ничего не дает, сама запись g(g) ввела в тупик, так как я не смог вывести в уме её тип. Оказывается если отложить типизацию в runtime, то можно позволить одной сущности иметь несколько типов: так каждый конкретный вызов функции k в рекурсии имеет свой тип.
0
Xitsa #
Ещё один факт, который я забыл отметить: вторая форма фундаментально отличается тем, что в ней формально нет рекурсии, а в первой — она есть.
Вторая форма классическая, и является базой, на которой определяют примитивную рекурсию.
–1
Lazin #
Интересно, кем нужно быть и чем заниматься что-бы это придумать!
+9
nwalker #
«Впервые понятие комбинатора и основанная на нём теория были сформалированны М.И.Шейнфинкелем в работе Schonfinkel (1924) ещё до появления лямбда-исчисления. Вскоре после этого аналогичные результаты были получены Карри, независимо от Шейнфинкеля и Чёрча. Когда Карри ознакомился с работами Шейнфинкеля, он предпринял попытку с ним связаться, но к этому времени Шейнфинкель оказался в психиатрической лечебнице.»

© Введение в функциональное программирование.
0
shai_xylyd #
Из Википедии следует, что Y-комбинатор — это частный случай функций высшего порядка, которые принимают на вход функцию f и возвращают функцию g, такую, что f(g)=g. Может это и так, но пользы для понимания это определение мне не дало


Перечитал пару раз статью в вики и мне открылось дао: Y-комбинатор используется для нахождения фиксированных функций функционала. Смысл в том, что если задача состоит в том, что бы написать функцию для подсчета факториала, то мы пишем такой функционал (отображение), что для функции факториала и только для неё он ведет себя как тождественный. Далее мы применяем Y-комбинатор к этому функционалу и получаем его фиксированную функцию, то есть факториал. То есть с помощью Y-комбинатора находим g (g=Y(f)) такую, что f(g)=g, а по построению функционала f этим свойством обладает только факториал.
0
Cancel #
У нас это называлось «неподвижными точками». А функционалом называлось отображение «из стульев в вещественные числа».

Как же всё-таки велика роль терминологии! Практически ничего не понял из статьи, но после чтения википедии всё прояснилось.
0
shai_xylyd #
Да да да. Не функционала, а оператора, заданного на функциях. Мне тоже математика проще как то на английском дается.
0
glader #
Я только не понял, зачем
test=lambda f:lambda n: 1 if n==0 else n*f(n-1)

если можно написать обычную рекурсию?
0
Xitsa #
Y–комбинатор больше концептуальная вещь, которая позволяет больше узнать о внутренних свойствах рекурсии. Например, для огранизации рекурсии, нет необходимости знать имени функции, поэтому приведён пример для анонимной (лямбда) функции. Можно управлять выполнением рекурсии, см. Ycache и Y, при этом, не меняя содержания функции (некий аналог MVC — такое же разбиение по ответственности).
0
Xitsa #
Перенёс в блог Python, так именно здесь в последнее время больше всего обсуждали функциональное программирование.
0
ivlis #
Респект автору! А разве на хабре нет блога посвящённому теоретическому программированию?
0
Cancel #
Я бы не назвал это теоретическим программированием, ибо теоретическое программирование довольно сложное и не очень укладывается в формат «общеобразовательного» блога, поскольку требует некоторых математических знаний.

А ФП, как ни парадоксально это звучит, очень прикладной предмет.
0
ivlis #
Не могу согласится. Теоретическая физика тоже очень сложна и требует огромного математического аппарата, но это не мешает писать популярные книжки про устройство космоса.
0
Cancel #
Ну так вот весь этот блог уже и есть аналог популярной книжки по теории программирования. Отдельные аспекты попадают в разные разделы сайта.
0
ivlis #
Казалось бы, причём тут питон? :)
0
redchrom #
Когда я разбирался с Y-комбинатором, мне очень понравилась эта статья dangermouse.brynmawr.edu/cs245/ycomb_jim.html
0
errr #
У Габриэла есть эссе The Why of Y, где все довольно популярно разъясняется.
+1
hodik #
С удовольствием читаю такие статьи на Хабре. Пока они есть, Хабр не будет банальным перечислением стартапов. Для тех, кто сомневается в практической применимости: почитайте про Эрланг — функциональный язык, на котором реализован чат фейсбука и другие примеры
0
UseShots #
Offtopic для небольшой разрядки:
конструкцию ...dumb(): pass" автоматически прочитал как dumbass ;-)
+1
qmax #
И вот вам ещё в тему эзотерического программирования: язык Iota, состоящий из единственного оператора. (комбинатора и ещё одного символа играющего роль скобок).

Язык сводится к исчислению KSI-комбинаторов (которое в свою очередь — к лямбда исчислению), и потому является тьюринг-полным.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.