Как работает головоломка Инь-Ян? - PullRequest
34 голосов
/ 23 апреля 2010

Я пытаюсь понять семантику call / cc в Scheme, и на странице Википедии, посвященной продолжениям, в качестве примера показана головоломка инь-ян:

(let* ((yin
         ((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))))
       (yang
         ((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))) )
    (yin yang))

Она должна вывести @*@**@***@****@...,но я не понимаю почему;Я ожидаю, что он выдаст @*@********* ...

Может кто-нибудь подробно объяснить, почему головоломка инь-ян работает так, как она работает?

Ответы [ 6 ]

19 голосов
/ 22 марта 2012

Понимание Схемы

Я думаю, что по крайней мере половина проблемы с пониманием этой загадки - синтаксис Схемы, с которым большинство не знакомо.

Прежде всего, я лично нахожу call/cc x сложнее для понимания, чем эквивалентная альтернатива, x get/cc.Он по-прежнему вызывает x, передавая ему текущее продолжение , но как-то более поддается представлению в схемах моего мозга.

Имея это в виду, конструкция (call-with-current-continuation (lambda (c) c)) становится просто get-cc.Теперь мы подошли к следующему:

(let* ((yin
         ((lambda (cc) (display #\@) cc) get-cc))
       (yang
         ((lambda (cc) (display #\*) cc) get-cc)) )
    (yin yang))

Следующим шагом является тело внутренней лямбды.(display #\@) cc, в более привычном синтаксисе (во всяком случае, для меня) означает print @; return cc;.Пока мы в этом, давайте также перепишем lambda (cc) body как function (arg) { body }, удалим несколько скобок и изменим вызовы функций на C-подобный синтаксис, чтобы получить это:

(let*  yin =
         (function(arg) { print @; return arg; })(get-cc)
       yang =
         (function(arg) { print *; return arg; })(get-cc)
    yin(yang))

Это начинаетсячтобы иметь больше смысла сейчас.Теперь это небольшой шаг, чтобы полностью переписать это в C-подобный синтаксис (или, например, в JavaScript), чтобы получить это:

var yin, yang;
yin = (function(arg) { print @; return arg; })(get-cc);
yang = (function(arg) { print *; return arg; })(get-cc);
yin(yang);

Теперь самое сложное закончено, мы расшифровали этоиз схемы!Просто шучу;это было только трудно, потому что у меня не было предыдущего опыта со Схемой.Итак, давайте выясним, как это на самом деле работает.

Учебник для продолжений

Обратите внимание на странно сформулированное ядро ​​инь и ян: оно определяет функцию , а затем немедленно вызывает ее.Это выглядит как (function(a,b) { return a+b; })(2, 3), который можно упростить до 5.Но упрощение вызовов внутри Инь / Ян было бы ошибкой, потому что мы не передаем это обычное значение.Мы передаем функцию продолжение .

Продолжение - странный зверь с первого взгляда.Рассмотрим гораздо более простую программу:

var x = get-cc;
print x;
x(5);

Первоначально x устанавливается на текущий объект продолжения (терпите меня), и print x выполняется, печатая что-то вроде <ContinuationObject>.Пока все хорошо.

Но продолжение похоже на функцию;это можно вызвать с одним аргументом.Что он делает: возьмите аргумент, а затем перейдите туда, где было создано это продолжение, восстановив весь контекст и сделав так, чтобы get-cc возвращал этот аргумент.

В нашем примереаргумент - 5, поэтому мы, по сути, возвращаемся прямо к середине этого оператора var x = get-cc, только на этот раз get-cc возвращает 5.Таким образом, x становится 5, и следующий оператор переходит к выводу 5. После этого мы пытаемся вызвать 5(5), что является ошибкой типа, и программа вылетает.

Заметьте, что вызовпродолжение - это прыжок , а не вызов.Он никогда не возвращается туда, где было вызвано продолжение.Это важно.

Как работает программа

Если вы следовали этому, не надейтесь: эта часть действительно самая сложная.Вот снова наша программа, отбрасывающая объявления переменных, потому что в любом случае это псевдокод:

yin = (function(arg) { print @; return arg; })(get-cc);
yang = (function(arg) { print *; return arg; })(get-cc);
yin(yang);

Первые строки времени 1 и 2 ударились, теперь они просты: получить продолжение, вызвать функцию (arg), напечатайте @, верните, сохраните это продолжение в yin.То же самое с yang.Теперь мы напечатали @*.

Далее мы вызываем продолжение в yin, передавая yang.Это заставляет нас перейти к строке 1, прямо внутри этого get-cc, и вместо этого вернуть yang.Значение yang теперь передается в функцию, которая печатает @, а затем возвращает значение yang.Теперь yin назначено то продолжение, которое имеет yang.Далее мы просто переходим к строке 2: получаем c / c, печатаем *, сохраняем c / c в yang.Теперь у нас есть @*@*.И наконец, мы переходим к строке 3.

Помните, что yin теперь имеет продолжение с момента, когда строка 2 была впервые выполнена.Итак, мы переходим к строке 2, печатая секунду * и обновляя yang.Теперь у нас есть @*@**.Наконец, снова вызовите продолжение yin, которое перейдет к строке 1, напечатав @.И так далее.Честно говоря, в этот момент мой мозг выдает исключение OutOfMemory, и я теряю все.Но, по крайней мере, мы добрались до @*@**!

Это труднои даже сложнее объяснить, очевидно. Идеальный способ сделать это - пройти через отладчик, который может представлять продолжение, но, увы, я не знаю ни о каком. Я надеюсь, вам понравилось это; Я конечно имею.

16 голосов
/ 24 апреля 2010

Я не думаю, что полностью понимаю это, но я могу думать только об одном ( чрезвычайно волнистом) объяснении этого:

  • Первые @ и * печатаются, когда yin и yang впервые связаны в let*. (yin yang) применяется и возвращается к началу сразу после завершения первого вызова / cc.
  • Следующие @ и * печатаются, затем печатается еще один *, потому что на этот раз yin повторно привязывается к значению второго вызова / cc.
  • (yin yang) применяется снова, но на этот раз выполняется в исходной среде yang , где yin связан с первым вызовом / cc, поэтому управление возвращается к печати другой @. Аргумент yang содержит продолжение, которое было перехвачено при втором проходе, что, как мы уже видели, приведет к печати **. Таким образом, на этом третьем проходе будет напечатано @*, затем будет запущено это продолжение печати с двумя звездами, так что в итоге получится 3 звезды, а затем это продолжение с тремя звездами будет перехвачено, ...
6 голосов
/ 09 апреля 2016

Пазл Инь Янь написан на схеме.Я предполагаю, что вы знаете основной синтаксис Схемы.

Но я предполагаю, что вы не знаете let* или call-with-current-continuation, я объясню эти два ключевых слова.

Объясните let*

Если вы уже знаете это, вы можете перейти к Explain call-with-current-continuation

let*, который выглядит как let, действует как let, но оценивает его определенные переменные ((yin ...) и (yang ...)) по очереди и с нетерпением.Это означает, что он сначала оценит yin, а затем yang.

. Подробнее читайте здесь: Использование схемы Let in

Объяснение call-with-current-continuation

Если вы уже это знаете, вы можете перейти к Yin-Yang puzzle.

Это немного сложно объяснить call-with-current-continuation.Поэтому я буду использовать метафору, чтобы объяснить это.

Представьте себе волшебника, который знал заклинание, которое было call-with-current-continuation.Как только он разыграет заклинание, он создаст новую вселенную и отправит себя в нее.Но он мог ничего не делать в новой вселенной, кроме как ждать, пока кто-нибудь позовет его по имени.Как только был назван , волшебник возвращался в исходную вселенную, имея бедного парня - «кого-то» - в руках, и продолжал свою жизнь волшебника.Если не было вызвано, когда новая вселенная закончилась, мастер также вернулся в исходную вселенную.

Хорошо, давайте будем более техническими.

call-with-current-continuation - это функция, которая принимает функцию какпараметр.Как только вы вызовете call-with-current-continuation с функцией F, она упакует текущую рабочую среду, которая называется current-continuation, в параметр C, и отправит ее в функцию F и выполнит F.Таким образом, вся программа становится (F C).Или быть более JavaScript: F(C);.C действует как функция.Если C не вызывается в F, то это обычная программа, когда F возвращает, call-with-current-continuation имеет значение в качестве возвращаемого значения F.Но если C вызывается с параметром V, это снова изменит всю программу.Программа возвращается в состояние при вызове call-with-current-continuation.Но теперь call-with-current-continuation дает значение, которое V.И программа продолжается.

Давайте рассмотрим пример.

(define (f return)
  (return 2)
  3)
(display (f whatever)) ;; 3
(display (call-with-current-continuation f)) ;; 2
(display (call-with-current-continuation (lambda (x) 4))) ;; 4

Первый display вывод 3, причины.

Но второй displayвывод 2.Почему?

Давайте окунемся в нее.

При оценке (display (call-with-current-continuation f)) сначала будет оцениваться (call-with-current-continuation f).Мы знаем, что он изменит всю программу на

(f C)

Учитывая определение для f, он имеет (return 2).Мы должны оценить (C 2).Вот когда continuation вызывается.Таким образом, вся программа вернется обратно к

(display (call-with-current-continuation f))

Но теперь call-with-current-continuation имеет значение 2.Таким образом, программа становится:

(display 2)

Головоломка Инь-Ян

Давайте посмотрим на головоломку.

(let* ((yin
         ((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))))
       (yang
         ((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))))
      (yin yang))

Давайте сделаем ее более читабельной.*

Давайте запустим программу в нашем мозгу.

Раунд 0

let* заставит нас сначала оценить yin.yin - это

(f (call-with-current-continuation id))

Итак, сначала мы оценим (call-with-current-continuation id).Он упаковывает текущую среду, которую мы называем C_0, чтобы отличить ее от другого продолжения на временной шкале, и входит в совершенно новую вселенную: id.Но id просто возвращает C_0.

Мы должны помнить, что такое C_0.C_0 - это программа, подобная этой:

(let* ((yin
         (f ###))
       (yang
         (g (call-with-current-continuation id))))
      (yin yang))

### - это заполнитель, который в будущем будет заполнен значением, которое C_0 возвращает.

Но id просто возвращает C_0.Это не звонит C_0.Если он позвонит, мы войдем во вселенную C_0.Но это не так, поэтому мы продолжаем оценивать yin.

(f C_0) ;; yields C_0

f - это функция, подобная id, но у нее есть побочный эффект - вывод @.

Итак, программа выведет @ и пусть yin будет C_0.Теперь программа становится

(let* ((yin C_0)
       (yang
         (g (call-with-current-continuation id))))
      (yin yang))

После оценки yin мы начинаем оценивать yang.yang is

(g (call-with-current-continuation id))

call-with-current-continuation здесь создайте еще одно продолжение с именем C_1.C_1 является:

(let* ((yin C_0)
       (yang
         (g ###)))
      (yin yang))

### является заполнителем.Обратите внимание, что в этом продолжении определяется значение yin (это то, что делает let*).Мы уверены, что значение yin здесь C_0.

Поскольку (id C_1) равно C_1, поэтому значение yang равно

(g C_1)

g имеет побочный эффект - вывод *.Так что программа делает.

yang значение теперь C_1.

К настоящему времени мы отобразили @*

Так что теперь оно становится:

(let* ((yin C_0)
       (yang C_1))
      (yin yang))

Поскольку и yin, и yang решены, мы должны оценить (yin yang).Это

(C_0 C_1)

Святой SH * T!

Но, наконец, C_0 называется.Итак, мы летим во вселенную C_0 и забываем все об этих дерьмах.Мы больше никогда не вернемся в эту вселенную.

Раунд 1

C_0 Возьми с C_1 назад.Теперь программа выглядит так: (Если вы забудете, что означает C_0, вернитесь, чтобы увидеть его):

(let* ((yin
         (f C_1))
       (yang
         (g (call-with-current-continuation id))))
      (yin yang))

Ах, мы обнаружим, что значение yin еще не определено.Поэтому мы оцениваем это.В процессе оценки yin мы выводим @ как побочный эффект f.И мы знаем, что yin сейчас C_1.

Мы начинаем оценивать yang, мы снова сталкиваемся с call-with-current-continuation.Мы практикуемся.Мы создаем продолжение C_2, которое обозначает:

(let* ((yin C_1)
       (yang
         (g ###)))
      (yin yang))

И мы отображаем * как g при выполнении.И мы приходим сюда

(let* ((yin C_1)
       (yang C_2))
      (yin yang))

Итак, мы получили:

(C_1 C_2)

Вы знаете, куда мы идем.Мы собираемся во вселенную C_1.Мы вспоминаем это из памяти (или копируем и вставляем с веб-страницы).Теперь это:

(let* ((yin C_0)
       (yang
         (g C_2)))
      (yin yang))

Мы знаем, что во вселенной C_1 определено значение yin.Итак, мы начинаем оценивать yang.Когда мы будем практиковаться, я прямо скажу вам, что он отображает * и становится:

(C_0 C_2)

Теперь мы напечатали @*@**, и мы собираемся во вселенную C_0 с C_2.

Раунд 2

По мере практики я скажу вам, что мы показываем '@', yin - это C_2, и мы создаем новое продолжение C_3, что означает:

(let* ((yin C_2)
       (yang
         (g ###)))
      (yin yang))

И мы отображаем *, yang это C_3, и оно становится

(C_2 C_3)

И мы можем продолжать.Но я на этом остановлюсь, я показал вам, какие первые несколько выводов головоломки Инь-Ян являются следующими:

Почему число * увеличивается?

Теперь ваша голова полна деталей.Я сделаю резюме для вас.

Я буду использовать подобный синтаксису Haskell для упрощения.И cc является сокращением от call-with-current-continuation.

Когда #C_i# заключен в скобки #, это означает, что продолжение здесь создается.; означает вывод


yin = f cc id
yang = g cc id
yin yang

---

yin = f #C_0# ; @
yang = g cc id
yin yang

---

yin = C_0
yang = g #C_1# ; *
yin yang

---

C_0 C_1

---

yin = f C_1 ; @
yang = g #C_2# ; *
yin yang

---

C_1 C_2

---

yin = C_0
yang = g C_2 ; *
yin yang

---

C_0 C_2

---

yin = f C_2 ; @
yang = g #C_3#; *
yin yang

---

C_2 C_3

---

yin = C_1
yang = g C_3 ; *
yin yang

---

C_1 C_3

---

yin = C_0
yang = g C_3 ; *
yin yang

---

C_0 C_3

Если вы внимательно наблюдаете, для вас будет очевидно, что

  1. Существует множество вселенных (фактически бесконечных), ноC_0 - единственная вселенная, основанная f.Другие запускаются g.
  2. C_0 C_n всегда делает новое продолжение C_max.Это потому, что C_0 - это первая вселенная, в которой g cc id было выполнено , а не .
  3. C_0 C_n также отображают @.C_n C_m Если n не равно 0, будет отображаться *.
  4. Время от времени программа выводится на C_0 C_n, и я докажу, что C_0 C_n отделяется все большим количеством других выражений, которыеприводит к @*@**@***...

Немного математики

Предположим, C_n (n! = 0) - это наибольшее число во всех продолжениях, и затем вызывается C_0 C_n.

Предположение: когда вызывается C_0 C_n, C_n - это текущее максимальное пронумерованное продолжение.

Теперь C_{n+1} создается C_0 C_n следующим образом:

yin = f C_n ; @
yang = g #C_{n+1}#
yin yang

Итак, мы заключаем, что:

Теорема I. Если вызывается C_0 C_n, то будет получено продолжение C_{n+1}, в котором yin равно C_n.

Тогда следующим шагом будет C_n C_{n+1}.

yin = C_{n-1}
yang = g C_{n+1} ; *
yin yang

Причина, по которой yin является C_{n-1}, заключается в том, что при создании C_n он подчиняется Теорема I .

И затем вызывается C_{n-1} C_{n+1}, и мы знаем, что когда создается C_{n-1}, он также подчиняется Теорема I .Итак, у нас C_{n-2} C_{n+1}.

C_{n+1} - это вариация.Итак, у нас есть вторая теорема:

Теорема II.Если C_n C_m который n < m и n > 0 называется, он станет C_{n-1} C_m.

И мы проверили вручную C_0 C_1 C_2 C_3. Они подчиняются предположению и всем теоремам. И мы знаем, как сначала создаются @ и *.

Итак, мы можем написать шаблоны ниже.

C_0 C_1 ; @ *
C_[1-0] C_2 ; @ * *
C_[2-0] C_3 ; @ * * *
...

Это не так строго, но я бы хотел написать:

1377 * что и требовалось доказать *

6 голосов
/ 25 апреля 2010

Сначала размышления, в конце возможный ответ.

Я думаю, что код можно переписать так:

; call (yin yang)
(define (yy yin yang) (yin yang))

; run (call-yy) to set it off
(define (call-yy)
    (yy
        ( (lambda (cc) (display #\@) cc) (call/cc (lambda (c) c)) )
        ( (lambda (cc) (display #\*) cc) (call/cc (lambda (c) c)) )
     )
)

Или с некоторыми дополнительными операторами отображения, чтобы помочь увидеть, что происходит:

; create current continuation and tell us when you do
(define (ccc)
    (display "call/cc=")
    (call-with-current-continuation (lambda (c) (display c) (newline) c))
)

; call (yin yang)
(define (yy yin yang) (yin yang))

; run (call-yy) to set it off
(define (call-yy)
    (yy
        ( (lambda (cc) (display "yin : ") (display #\@) (display cc) (newline) cc) 
            (ccc) )
        ( (lambda (cc) (display "yang : ") (display #\*) (display cc) (newline) cc) 
            (ccc) )
     )
)

Или вот так:

(define (ccc2) (call/cc (lambda (c) c)) )
(define (call-yy2)
    (
        ( (lambda (cc) (display #\@) cc) (ccc2) )
        ( (lambda (cc) (display #\*) cc) (ccc2) )
    )
)

Возможный ответ

Возможно, это неправильно, но я пойду.

Я думаю, что ключевым моментом является то, что «вызываемое» продолжение возвращает стек в какое-то предыдущее состояние - как будто ничего больше не произошло. Конечно, он не знает, что мы отслеживаем его, отображая символы @ и *.

Мы изначально определяем yin как продолжение A, которое будет делать это:

1. restore the stack to some previous point
2. display @
3. assign a continuation to yin
4. compute a continuation X, display * and assign X to yang
5. evaluate yin with the continuation value of yang - (yin yang)

Но если мы назовем продолжение yang, это произойдет:

1. restore the stack to some point where yin was defined
2. display *
3. assign a continuation to yang
4. evaluate yin with the continuation value of yang - (yin yang)

Мы начинаем здесь.

В первый раз вы получаете yin=A и yang=B, поскольку yin и yang инициализируются.

The output is @*

(Продолжения A и B вычисляются.)

Теперь (yin yang) впервые оценивается как (A B).

Мы знаем, что делает A. Это делает это:

1. restores the stack - back to the point where yin and yang were being initialised.
2. display @
3. assign a continuation to yin - this time, it is B, we don't compute it.
4. compute another continuation B', display * and assign B' to yang

The output is now @*@*

5. evaluate yin (B) with the continuation value of yang (B')

Теперь (yin yang) оценивается как (B B').

Мы знаем, что делает B. Это делает это:

1. restore the stack - back to the point where yin was already initialised.
2. display *
3. assign a continuation to yang - this time, it is B'

The output is now @*@**

4. evaluate yin with the continuation value of yang (B')

Поскольку стек был восстановлен до точки, в которой yin=A, (yin yang) оценивается как (A B').

Мы знаем, что делает A. Это делает это:

1. restores the stack - back to the point where yin and yang were being initialised.
2. display @
3. assign a continuation to yin - this time, it is B', we don't compute it.
4. compute another continuation B", display * and assign B" to yang

The output is now @*@**@*

5. evaluate yin (B') with the continuation value of yang (B")

Мы знаем, что делает B'. Это делает это:

1. restore the stack - back to the point where yin=B.
2. display *
3. assign a continuation to yang - this time, it is B"

The output is now @*@**@**

4. evaluate yin (B) with the continuation value of yang (B")

Теперь (yin yang) оценивается как (B B").

Мы знаем, что делает B. Это делает это:

1. restore the stack - back to the point where yin=A and yang were being initialised.
2. display *
3. assign a continuation to yang - this time, it is B'"

The output is now @*@**@***

4. evaluate yin with the continuation value of yang (B'")

Поскольку стек был восстановлен до точки, в которой yin=A, (yin yang) оценивается как (A B'").

.......

Я думаю, что у нас сейчас есть образец.

Каждый раз, когда мы вызываем (yin yang), мы перебираем стек продолжений B, пока не вернемся к моменту yin=A и не отобразим @. Мы перебираем в стеке B продолжений, каждый раз записывая *.

(я был бы очень рад, если бы это было приблизительно правильно!)

Спасибо за вопрос.

1 голос
/ 09 апреля 2016

Это старая загадка от мастера запутывания Дэвида Мадора, который создал Unlambda. Головоломка была обсуждена несколько раз. раз.

Хорошее решение от Тейлора Кэмпбелла: https://groups.google.com/d/msg/comp.lang.scheme/pUedvrKYY5w/uIjTc_T1LOEJ

Оригинальный пост от Дэвида Мадора (1999): https://groups.google.com/d/msg/comp.lang.scheme/Fysq_Wplxsw/awxEZ_uxW20J

1 голос
/ 13 августа 2015

Как сказал другой ответ, мы сначала упростим (call-with-current-continuation (lambda (c) c)) с get-cc.

(let* ((yin
         ((lambda (cc) (display #\@) cc) get-cc))
       (yang
         ((lambda (cc) (display #\*) cc) get-cc)) )
    (yin yang))

Теперь две лямбда - это просто идентичные функции, связанные с побочными эффектами. Давайте назовем эти функции f (для display #\@) и g (для display #\*).

(let* ((yin (f get-cc))
       (yang (g get-cc)))
    (yin yang))

Далее нам нужно выработать порядок оценки. Чтобы было ясно, я введу «выражение шага», которое делает каждый шаг оценки явным. Для начала давайте спросим: для чего нужна вышеуказанная функция?

Требуются определения f и g. В шаговом выражении мы пишем

s0 f g =>

Первым шагом является вычисление yin, но для него требуется оценка (f get-cc), а для более позднего - get-cc.

Грубо говоря, get-cc дает значение, которое представляет «текущее продолжение». Допустим, это s1, так как это следующий шаг. Итак, мы пишем

s0 f g => s1 f g ?
s1 f g cc =>

Обратите внимание, что параметры не имеют области видимости, что означает, что f и g в s0 и s1 необязательны, и они должны использоваться только на текущем шаге. Это делает контекстную информацию явной. Теперь, каково значение для cc? Так как это «текущее продолжение», оно является одним и тем же s1 с f и g, привязанными к одному и тому же значению.

s0 f g => s1 f g (s1 f g)
s1 f g cc =>

Как только мы получим cc, мы можем оценить f get-cc. Кроме того, поскольку f не используется в следующем коде, нам не нужно передавать это значение.

s0 f g => s1 f g (s1 f g)
s1 f g cc => s2 g (f cc)
s2 g yin =>

Следующее похоже на yang. Но теперь у нас есть еще одно значение для передачи: yin.

s0 f g => s1 f g (s1 f g)
s1 f g cc => s2 g (f cc)
s2 g yin => s3 g yin (s3 g yin)
s3 g yin cc => s4 yin (g cc)
s4 yin yang => 

Наконец, последний шаг - применить yang к yin.

s0 f g => s1 f g (s1 f g)
s1 f g cc => s2 g (f cc)
s2 g yin => s3 g yin (s3 g yin)
s3 g yin cc => s4 yin (g cc)
s4 yin yang => yin yang

На этом закончена конструкция поэтапного выражения. Перевести его обратно на схему просто:

(let* ([s4 (lambda (yin yang) (yin yang))]
       [s3 (lambda (yin cc) (s4 yin (g cc))]
       [s2 (lambda (yin) (s3 yin ((lambda (cc) (s3 yin cc))))]
       [s1 (lambda (cc) (s2 (f cc)))])
      (s1 s1))

Подробный порядок оценки (здесь лямбда внутри тела s2 была просто выражена как частичная оценка s3 yin, а не (lambda (cc) (s3 yin cc))):

(s1 s1)
=> (s2 (f s1))
=> @|(s2 s1)
=> @|(s3 s1 (s3 s1))
=> @|(s4 s1 (g (s3 s1)))
=> @*|(s4 s1 (s3 s1))
=> @*|(s1 (s3 s1))
=> @*|(s2 (f (s3 s1)))
=> @*@|(s2 (s3 s1))
=> @*@|(s2 (s3 s1))
=> @*@|(s3 (s3 s1) (s3 (s3 s1)))
=> @*@|(s4 (s3 s1) (g (s3 (s3 s1))))
=> @*@*|(s4 (s3 s1) (s3 (s3 s1)))
=> @*@*|(s3 s1 (s3 (s3 s1)))
=> @*@*|(s4 s1 (g (s3 (s3 s1))))
=> @*@**|(s4 s1 (s3 (s3 s1)))
=> @*@**|(s1 (s3 (s3 s1)))
=> ...

(Помните, что при оценке s2 или s4 параметр будет оцениваться первым

...