Scheme для троллей, лжецов и ...
От: Mr.Cat  
Дата: 22.06.09 18:08
Оценка: 2 (2)
Когда в decl появилась эта ветка: http://rsdn.ru/forum/decl/3431144.aspx
Автор: achmed
Дата: 17.06.09
— я подумал, что неплохо бы поближе познакомить читателей rsdn со scheme. Пожалуй, лучшим для этого форматом были бы краткие примеры реализации какой-то интересной функциональности. Начну я с пары-тройки вариантов корутин — ну а дальше уж как получится, если получится вообще.

Впрочем, какой я к черту евангелист scheme? Скорее просто тролль. Так что давайте притворимся, что ваш покорный слуга просто форсит на форуме единственный язык, который знает, и не будем сильно пинать его за ошибки.
Re: 1. Простые питоноподобные генераторы
От: Mr.Cat  
Дата: 22.06.09 18:10
Оценка: 48 (7)
Сегодня мы реализуем на scheme аналог генераторов из питона. Наш коллега palm mute уже приводил пример реализации в своем блоге: http://palm-mute.livejournal.com/12291.html, и сегодня мы не сильно выйдем за рамки этого поста.

В качестве ленивых списков мы будем использовать потоки из srfi-41. Возможно, это не самая хорошая идея, однако для демонстрации возможностей — вполне подойдет.

Главным нашим инструментом будет shift/reset. Reset, подобно begin, выделяет группу выражений. Shift позволяет прервать выполнение этой группы выражений и сразу выскочить за пределы reset.

В plt scheme, которой мы будем пользоваться, как shift/reset, так и потоки уже реализованы. Нам будет достаточно подключить два модуля.
(require scheme/control)
(require srfi/41)

Ну а теперь — небольшой пример
(display (reset
          (display 1)
          (display 2)
          (shift k 3)
          (display 4)))

>123

Основной профит shift заключен в его первом параметре. Первый параметр shift — континуация, невыполненная часть блока reset. С этой континуацийе можно обращаться как с любой другой функцией: вызывать, сохранять где-нибудь на будущее или (как в предыдущем примере) просто выкинуть.
Таким образом понятно, что должен делать yield — захватывать континуацию генератора с помощью shift и сохранять ее в stream-cdr.
(stream->;list (reset
               (shift k (stream-cons 1 (k (void))))
               (shift k (stream-cons 2 (k (void))))
               (shift k (stream-cons 3 (k (void))))
               stream-null))

>(1 2 3)

Стоит заметить, что при данной семантике yield последним выражением в reset (если, конечно, генератор вообще предполагает выход из блока reset) должен быть stream-null.

Итак, для определения генератора мы напишем такой вот несложный макрос. Во-первых, он заключает тело функции в reset, а во-вторых, заставляет ее всегда возвращать stream-null.
(define-syntax define-generator
  (syntax-rules ()
    ((_ (name args ...) body ...)
     (define (name args ...)
       (reset body ... stream-null)))))

А yield тогда будет обычной функцией.
(define (yield value)
  (shift k (stream-cons value (k (void)))))

Вуаля
(define-generator (make-123)
  (yield 1)
  (yield 2)
  (yield 3))

(stream->;list (make-123))

>(1 2 3)

Ну и последний на сегодня штрих. Определим хелпер, который позволит из генератора обращаться к другим генераторам (и к себе рекурсивно) и встраивать их выхлоп в собственный.
(define (yield-splice stream)
  (shift k (stream-append stream (k (void)))))

(define-generator (make-0-4)
  (yield 0)
  (yield-splice (make-123))
  (yield 4))

(stream->;list (make-0-4))

>(0 1 2 3 4)

На сегодня все, однако с питоновскими генераторами мы еще не закончили. Ведь что делает их по-настоящему интересными — так это возможность извне влиять на выполнение генератора с помощью send(). Send() позволяет указать значение, которое внутри генератора вернет yield. У нас yield возвращает то, что в качестве параметра передается в пойманную шифтом континуацию. То есть мы должны научиться вместо (void) передавать туда что-то полезное. Но это в следующий раз.
Re[2]: 1. Простые питоноподобные генераторы
От: MigMit Россия http://migmit.vox.com
Дата: 22.06.09 21:34
Оценка:
Здравствуйте, Mr.Cat, Вы писали:

MC>Сегодня мы реализуем на scheme аналог генераторов из питона.


(краснея) А можно пару примеров того, что такое генераторы из питона?
Re[3]: 1. Простые питоноподобные генераторы
От: FR  
Дата: 23.06.09 02:59
Оценка:
Здравствуйте, MigMit, Вы писали:

MM>(краснея) А можно пару примеров того, что такое генераторы из питона?



def fib(n):
    a, b = 1, 1
    for i in range(n):
        yield a
        a, b = b, a + b
        
for i in fib(10):
    print i
Re[3]: 1. Простые питоноподобные генераторы
От: FR  
Дата: 23.06.09 03:01
Оценка:
Здравствуйте, MigMit, Вы писали:

MM>(краснея) А можно пару примеров того, что такое генераторы из питона?


http://www.iso.ru/journal/articles/print/155.html
Re[4]: 1. Простые питоноподобные генераторы
От: Mr.Cat  
Дата: 23.06.09 05:11
Оценка: 12 (1)
Здравствуйте, FR, Вы писали:
FR>
FR>def fib(n):
FR>    a, b = 1, 1
FR>    for i in range(n):
FR>        yield a
FR>        a, b = b, a + b
        
FR>for i in fib(10):
FR>    print i
FR>


А наш вариант на scheme позволит писать вот так (пожалуй, стоило включить подобный пример в текст):
(define-generator (fib)
  (let fib/iter ((curr 1)
                      (next 1))
    (yield curr)
    (fib/iter next (+ curr next))))

Результатом этого выражения будет бесконечный стрим чисел фибоначчи.
Можно было бы еще рекурсивно извратиться с помощью yield-splice, однако в нашем варианте невозможны хвостовые вызовы между генераторами, так что за пределы генератора лучше лишний раз не выходить.
Re[4]: 1. Простые питоноподобные генераторы
От: achmed Удмуртия https://www.linkedin.com/in/nail-achmedzhanov-9907188/
Дата: 23.06.09 06:56
Оценка:
Здравствуйте, FR, Вы писали:

FR>Здравствуйте, MigMit, Вы писали:


MM>>(краснея) А можно пару примеров того, что такое генераторы из питона?


FR>

FR>def fib(n):
FR>    a, b = 1, 1
FR>    for i in range(n):
FR>        yield a
FR>        a, b = b, a + b
        
FR>for i in fib(10):
FR>    print i

FR>


И в Python 2.5 появилась возможность передавать данные в итератор:
def counter (maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1


>>> it = counter(10)
>>> print it.next()
0
>>> print it.next()
1
>>> print it.send(8)
8
>>> print it.next()
9
>>> print it.next()
Traceback (most recent call last):
  File "t.py", line 15, in ?
    print it.next()
StopIteration


http://docs.python.org/whatsnew/2.5.html
http://www.python.org/dev/peps/pep-0342/
Re: Scheme для троллей, лжецов и ...
От: DemAS http://demas.me
Дата: 23.06.09 07:16
Оценка:
Спасибо огромное.
Еще не начал читать, но буду делать это обязательно.
Среда DrScheme подойдет для проверки описанного?
Posted via RSDN NNTP Server 2.1 beta
Re[2]: 1. Простые питоноподобные генераторы
От: DemAS http://demas.me
Дата: 23.06.09 07:26
Оценка:
>
>; (require scheme/control)
>; (require srfi/41)
>;


А можно перед каждым примером указывать language, который нужно
использовать в drscheme? Я, например, выставил R5RS и получил сообщение
об ошибке:

reference to undefined identifier: require

Posted via RSDN NNTP Server 2.1 beta
Re[3]: 1. Простые питоноподобные генераторы
От: Mr.Cat  
Дата: 23.06.09 07:57
Оценка:
Здравствуйте, DemAS, Вы писали:
DAS>А можно перед каждым примером указывать language, который нужно
DAS>использовать в drscheme?
В качестве language надо выбрать module. В начало файла, возможно, придется дописать #lang scheme.
Re[5]: 1. Простые питоноподобные генераторы
От: Spiceman  
Дата: 23.06.09 08:15
Оценка:
Здравствуйте, Mr.Cat, Вы писали:

MC>А наш вариант на scheme позволит писать вот так (пожалуй, стоило включить подобный пример в текст):

MC>
MC>;(define-generator (fib)
MC>;  (let fib/iter ((curr 1)
MC>;                      (next 1))
MC>;    (yield curr)
MC>;    (fib/iter next (+ curr next))))
MC>;

MC>Результатом этого выражения будет бесконечный стрим чисел фибоначчи.
MC>Можно было бы еще рекурсивно извратиться с помощью yield-splice, однако в нашем варианте невозможны хвостовые вызовы между генераторами, так что за пределы генератора лучше лишний раз не выходить.

Я не спец, извиняюсь за глупые вопросы, но почему нельзя было просто написать бесконечный поток чисел фибоначчи? Зачем нам обязательно yield? Какое оно дает преимущество?
Re[6]: 1. Простые питоноподобные генераторы
От: Mr.Cat  
Дата: 23.06.09 08:45
Оценка:
Здравствуйте, Spiceman, Вы писали:
S>почему нельзя было просто написать бесконечный поток чисел фибоначчи?
Потому что мы сперва решили реализовать аналог yield, а потом стали придумывать, куда бы его впихнуть.

S>Зачем нам обязательно yield? Какое оно дает преимущество?

Да никакого особо преимущества. Просто другой стиль.
Re[5]: 1. Простые питоноподобные генераторы
От: DemAS http://demas.me
Дата: 23.06.09 08:46
Оценка:
> Результатом этого выражения будет бесконечный стрим чисел фибоначчи.

А как из этого генератора получить необходимый нам диапазон чисел?

 (stream->;list (fib))
— приводит к бесконечной рекурсии
Posted via RSDN NNTP Server 2.1 beta
Re[5]: 1. Простые питоноподобные генераторы
От: DemAS http://demas.me
Дата: 23.06.09 08:46
Оценка:
>
>; (define-generator (fib)
>;   (let fib/iter ((curr 1)
>;                       (next 1))
>;     (yield curr)
>;     (fib/iter next (+ curr next))))
>;


И еще, что такое fib/iter? Что такое let я представляю, но как работает
конструкция

> (let fib/iter ((curr 1)

> (next 1))

уже нет.
Posted via RSDN NNTP Server 2.1 beta
Re[6]: 1. Простые питоноподобные генераторы
От: Mr.Cat  
Дата: 23.06.09 08:49
Оценка:
Здравствуйте, DemAS, Вы писали:
DAS>А как из этого генератора получить необходимый нам диапазон чисел?
Например, с помощью stream-take.
Re[6]: 1. Простые питоноподобные генераторы
От: Mr.Cat  
Дата: 23.06.09 08:50
Оценка:
Здравствуйте, DemAS, Вы писали:

>>
>;> (define-generator (fib)
>;>   (let fib/iter ((curr 1)
>;>                       (next 1))
>;>     (yield curr)
>;>     (fib/iter next (+ curr next))))
>;>


DAS>И еще, что такое fib/iter? Что такое let я представляю, но как работает

DAS>конструкция

>> (let fib/iter ((curr 1)

>> (next 1))

DAS>уже нет.

Это named let
Re[7]: 1. Простые питоноподобные генераторы
От: DemAS http://demas.me
Дата: 23.06.09 10:36
Оценка:
Mr.Cat wrote:
> Здравствуйте, DemAS, Вы писали:
> DAS>А как из этого генератора получить необходимый нам диапазон чисел?
> Например, с помощью stream-take.

Через stream->list похоже тоже можно:

 (stream->;list 10 (fib))
Posted via RSDN NNTP Server 2.1 beta
Re: Scheme для троллей, лжецов и ...
От: GSergey  
Дата: 25.06.09 17:49
Оценка: 12 (1)
Здравствуйте, Mr.Cat, Вы писали:

MC>Когда в decl появилась эта ветка: http://rsdn.ru/forum/decl/3431144.aspx
Автор: achmed
Дата: 17.06.09
— я подумал, что неплохо бы поближе познакомить читателей rsdn со scheme. Пожалуй, лучшим для этого форматом были бы краткие примеры реализации какой-то интересной функциональности. Начну я с пары-тройки вариантов корутин — ну а дальше уж как получится, если получится вообще.


MC>Впрочем, какой я к черту евангелист scheme? Скорее просто тролль. Так что давайте притворимся, что ваш покорный слуга просто форсит на форуме единственный язык, который знает, и не будем сильно пинать его за ошибки.


А вот другой пример, как делать "не надо", это будет работать в SCM

(defmacro generator () `(define (_generator)(call/cc _control-state)))
(defmacro start-generator ( end-value . code ) `(define (_control-state _return) ,@code (_return ,end-value)))
(defmacro end-generator () '_generator)
(defmacro yeild (value) `(set! _return
(call/cc (lambda (_resume-here)
(set! _control-state _resume-here)
(_return ,value)))))
(defmacro make-generator-fast (input finish . code) `((lambda (,input) (generator) (start-generator ,finish ,@code)(end-generator)) ,input))

А это примеры, make-gen1 просто yeild для for-each

(define (make-gen1 list)
(make-generator-fast list '()
(for-each (lambda (element)
(yeild element))
list)))

А вот это уже пример интереснее, позволядет проитетироватья по дереву

(define (make-gen2 tree)
(make-generator-fast tree '()
(begin
(define (for-tree tree)
(if (null? tree) '()
(let ((head (car tree)))
(if (not (pair? head)) (yeild head) (for-tree head))
(for-tree (cdr tree)))))
(for-tree tree))))

(define (test)
(define g (make-gen2 '((1 2) (3 4)((5)) 6)))
(display (g))
(display (g))
(display (g))
(display (g))
(display (g)))

выводит 1 2 3 4 5 6
Re: Scheme для троллей, лжецов и ...
От: frogkiller Россия  
Дата: 26.06.09 08:41
Оценка:
Здравствуйте, Mr.Cat, Вы писали:

MC>Когда в decl появилась эта ветка: http://rsdn.ru/forum/decl/3431144.aspx
Автор: achmed
Дата: 17.06.09
— я подумал, что неплохо бы поближе познакомить читателей rsdn со scheme. Пожалуй, лучшим для этого форматом были бы краткие примеры реализации какой-то интересной функциональности. Начну я с пары-тройки вариантов корутин — ну а дальше уж как получится, если получится вообще.


MC>Впрочем, какой я к черту евангелист scheme? Скорее просто тролль. Так что давайте притворимся, что ваш покорный слуга просто форсит на форуме единственный язык, который знает, и не будем сильно пинать его за ошибки.


Несколько лет назад была у меня такая же идея, но потом что-то заглохло

Вот сейчас откопал с тех времён самописный чисто функциональный пузырёк:
(define (bubblesort pred arglist)
    (define (cmppr pr al) (and (pair? al) (pair? (cdr al)) (pr (car al) (cadr al))))
    (define (iter pr al)
        (let ((forw (if (pair? al) (cons (car al) (iter pr (cdr al)) ))))
            (if (cmppr pr forw) 
                (let ((swapped (cons (cadr forw) (cons (car forw) (cddr forw)))))
                   (let ((backw (iter pr (cdr swapped)) ))
                       (iter pr (cons (car swapped) backw))
                   )
                ) 
                forw
            )
        )
    )
    (iter pred arglist)
)

(bubblesort < (list 7 1 4 2 9))


Кстати, поскольку здесь нет "!"-команд, то его сложность должна быть O(n^3), если я не ошибаюсь.
Курица — это инструмент, с помощью которого одно яйцо производит другие.
Re: 2. Функция, сохраняющая состояние
От: Mr.Cat  
Дата: 26.06.09 20:38
Оценка: 13 (2)
И снова здрасьте. Сегодня я хотел продолжить компостировать вам мозги генераторами и потоками, однако наш коллега GSergey обратил наше внимание () на другой вариант семантики корутин. Вот примерно такой:
(require mzlib/defmacro)

(defmacro generator ()
  `(define (_generator)
     (call/cc _control-state)))

(defmacro start-generator ( end-value . code )
  `(define (_control-state _return)
     ,@code (_return ,end-value)))

(defmacro end-generator ()
  '_generator)

(defmacro yeild (value)
  `(set! _return
         (call/cc (lambda (_resume-here)
                    (set! _control-state _resume-here)
                    (_return ,value)))))

(defmacro make-generator-fast (input finish . code)
  `((lambda (,input)
      (generator)
      (start-generator ,finish ,@code)
      (end-generator)) ,input))

Действительно. Что если "научить" нашу функцию возвращать значение и при этом сохранять свое состояние и при следующем вызове к этому состоянию возвращаться? По-моему, идея хороша. К этой идее мы подойдем с той же стороны, что и наш коллега, с небольшими отличиями в деталях. Кстати, заранее прошу прощения за то, что rsdn в блоках кода заменяет -> на ->;.

Нам снова потребуется shift/reset:
(require scheme/control)

То, что GSergey реализовывал с помощью пары call/cc, мы сделаем через shift/reset. Тело нашей "обладающнй состоянием" функции мы снова заключим в reset, а yield снова будет довольно несложной оберткой над shift. Но на этот раз пойманную шифтом континуацию мы будем просто просто сохранять — на будущее. Итак, начнем писать макрос для определения обладающей состоянием функции, и по ходу дела разберемся с частностями.
(define-syntax stateful-lambda
  (syntax-rules ()    
    ((_ yield fin args body ...)
     ;;В state мы будем хранить континуацию, если функция уже вызывалась
     ;;и была прервана йилдом и #f в противном случае.
     
     ;;Функцию yield будет на этот раз для каждой лямбды своя, поскольку
     ;;она должна иметь доступ к специфичному для каждой лямбды state

     ;;При таком раскладе гигиеничность системы макросов заставляет нас
     ;;явно передавать в макрос имя, которое будет использоваться для yield
     ;;в качестве параметра.
     (let* ((state #f)
            (yield (lambda v (shift k (begin (set! state k)
                                             (apply values v))))))
       (lambda args1
         ;;Если в состоянии лежит захваченная ранее шифтом континуация -
         ;;мы просто ее вызовем
         (if state
             (apply state (if (= (length args1) 0)
                              (list (void))
                              args1))
             ;;При первом же вызове лямбды-с-состоянием мы просто начнем выполнять
             ;;ее тело.

             ;;При выходе из лямбды-с-состоянием не посредством yield, мы обнулим ее
             ;;состояние и при этом возвратим специальное значение fin, которое мы
             ;;также указали при объявлении лямбды.
             (apply (lambda args (reset body ...
                                        (set! state #f)
                                        fin)) args1)))))))

Добавим к перечисленному более-менее корректную обработку множественных возвращаемых значений — и наш макрос готов к использованию.

Так что теперь — небольшой такой, зато кривой пример. Напишем функцию list-walker, которая, принимая список, возвратит stateful-lambda, которая будет по очереди возвращать элементы этого списка. И когда они закончатся — начнет с начала.
(define (list-walker l)
  (let ((head (drop-right l 1))
        (tail (last l)))
    (stateful-lambda yield tail ()                   
                     (for-each yield head))))

Теперь мы умеем писать функции, которые умеют поддерживать внутри себя состояние, скрывая его от внешнего мира. А когда мы говорим "состояние", на ум сразу приходит что? Правильно, конечный автомат. Так что давайте поверх нашей функции сделаем хелпер для реализации конечных автоматов.
(define-syntax fsm-lambda
  (syntax-rules (->;)
    ;;При создании автомата будем задавать начальное состояние и конечное.
    
    ;;С начальным все понятно. Конечное мы выделяем затем, чтобы бросать
    ;;искючение всегда, когда в это состояние приходит сообщение
    ((_ start fin
        ;;Правила перехода будем задавать в виде
        ;;(состояние сообщение -> (новое-состояние . возвращаемое-значение))
        ;;Понятное дело, что после стрелки не обязательно должна стоять
        ;;непосредственно пара, достаточно возвращающего ее выражения
        (from message ->; to) ...)
     (stateful-lambda yield final (message1)
                      (let iter ((state start)
                                 (message2 message1))
                        (let ((result
                               (match (cons state message2)
                                      ((cons fin _) (error 'fin))
                                      ((cons from message) to) ...)))
                          (iter (car result) (yield (cdr result)))))))))

Ну и пример, конечно. И да, снова тривиальный. Сделаем счетчик, который можно перезапускать по команде 'restart.
(define counter
  (fsm-lambda 0 'foo
              (value 'restart ->; (cons 0 0))
              (value increment ->; (let ((newvalue (+ value increment)))
                                    (cons newvalue newvalue)))))

> (counter 1)
1
> (counter 1)
2
> (counter 1)
3
> (counter 1)
4
> (counter 1)
5
> (counter 'restart)
0
> (counter 1)
1
> (counter 1)
2

На этом примере, кстати, видно, что автомат, который у нас получился, он вовсе даже и не конечный. Такие дела.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.