Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Fp_8.doc
Скачиваний:
1
Добавлен:
01.05.2025
Размер:
116.22 Кб
Скачать

8. Потоки и ленивые вычисления

Представление последовательностей списками приводит к таким большим затратам, что может дискредитировать саму идею их применение в качестве стандартных интерфейсов.

Однако, ситуация вовсе не безнадёжна. Нам просто нужно выбрать представление, исключающее расточительное использование ресурсов.

Рассмотрим реализацию последовательностей, обычно называемую потоками (англ. streams).

8.1 Потоки

Основная идея организации потоков состоит в том, чтобы создавать не всю огромную последовательность данных, а только некую её часть, и только её передавать в потребляющую  процедуру. Но когда потребуется обратиться к той части, которая ещё не была создана, последовательность автоматически достраиваться.

Для реализации такой идеи программы должны писаться так, будто происходит обработка полной последовательности (потока) данных, хотя на самом деле недостающие данные будут появлятся в процессе выполнения и лишь по мере необходимости.

С абстрактной точки зрения потоки похожи на списки. И также как для списков набор cons, nil, car, cdr, empty? , для потока  существует соответствующий  набор из конструкторов (cons-stream empty-stream) и селекторов ( head tail empty-stream?).

Условия, которым должны удовлетворять эти функции совпадают с теми, которые выполнятся для списков, но есть существенное различие – это период времени, когда вычисляются элементы последовательности.

Для списка хвост вычисляются при создании списка.

Хвост потока вычисляется только в момент использования.

Для реализации потоков обращаются к специальной форме называемой задержкой (англ. delay). Вычисления, использующие задержку называются задержанными вычислениями

Эта функция, называемая delay, не вычисляет указываемое за ней выражение, а возвращает специальный объект – promise (англ. обещание), т.е. обещание вычислить значение этого выражения, как только оно будет востребовано.

Другая процедура force (англ. вынуждать), получая в качестве аргумента определённый таким способом объект, выполняет обещание и вычисляет значение выражения.

Вот пример использования функции (delay expr). Сочиним функцию сложения с задержкой двух конкретных чисел.

> (define add2+3 (delay (+ 2 3)))

Попробуем их всё-таки сложить.

> add2+3

#<promise:add2+3>

Видно, что никакого сложения не происходит, а лишь сообщение, что это сложение может быть выполнено, но если его заставить. Теперь приходит черёд обращения к (force expr), чтобы «реально» вызвать исполнение намеченного сложения .

> (force add2+3)

5

Итак, на конкретном примере мы проиллюстрировали понятие задержанных вычислений.

Разберём, как это реализуется. Оказывается, что потоки, как и списки, строятся, опираясь на пары. Однако, в отличие от списков, второй элемент пары содержит не оставшуюся часть последовательности, а «лишь обещание» вычислить эту часть, если она когда-либо потребуется.

Теперь надо раскрыть две главные процедуры потоков - head и tail:

> (define (head S) (car S))

> (define (tail S) (force (cdr S)))

Процедура head просто выбирает первый элемент пары, в то время как tail выбирает второй элемент пары и вынуждает получить остальную часть потока, т.е. делает вычисление отложенного выражения.

Понятие пустого потока можно совместить с понятием пустого списка.

> (define empty-stream '() )

> (define empty-stream? null?)

Специальная форма cons-stream  определяется так, что(cons-stream a b) эквивалентно (cons a (delay b)). Но обратите внимание, это только эквивалентность по мысли!! Нельзя определить cons-stream именно так: явно, как процедуру. Потому что если записать

(define (cons-stream a b) (cons a (delay b)),

то при обращении к (cons-stream a b) значения a и b сразу же будут вычислены!! И применение задержки потеряет всякий смысл.

Как же все-таки тогда определить эту функцию?

Нам придется воспользоваться механизмом встроенных в DrScheme макросов.

Воспользуемся двумя функциями

define-syntax и syntax-rules без подробного объяснения. Потому что, если взяться за объяснения этих функций, нужно будет сильно углубиться в возможности конкретной реализации ЛИСПа, а как уже оговаривалось в начале: «нашей целью изучения функционального программирования является не конкретный язык, а лишь только методы функционального программирования!» Не принимающие такой ответ могут обратиться к учебнику по DrScheme или встроенному Help’у через меню: Help > Help disk > и далее набрать названия этих функций для поиска.

Итак, запишем определение:

> (define-syntax cons-stream

(syntax-rules () ( (_ a b) (cons a (delay b)) ) ))

Придется поверить на слово, что такое макрос-определение cons-stream дает возможность преобразовать всякое вхождение выражения вида (cons-stream a b) в символьное (а стало быть, не сразу выполняемое) выражение (cons a (delay b)).

Пример обращения.

> (cons-stream 'x 'y)

(x . #<promise:...)

Придумаем поток s_abc из трёх символов a, b и c и создадим его как

(define s_abc

(cons-stream 'a

(cons-stream 'b

(cons-stream 'c empty-stream))))

Согласно макросу это будет преобразовано в следующее определение

(define s_abc

(cons 'a

(delay (cons 'b

(delay (cons 'c

(delay empty-stream))))

С потоками надо общаться так же,  как и со списками, но используя соответствующие по смыслу функции.

> s_abc

(a . #<promise:...)

> (head s_abc)

a

> (head (tail s_abc))

b

Создадим функцию выборки элемента под номером n из потока S

> (define (elem-no n S)

(if (= n 1) (head S)

(elem-no (- n 1) (tail S))))

> (elem-no 1 s_abc)

a

> (elem-no 2 s_abc)

b

Интересно сравнить две взаимно противоположные функции, преобразующие список в поток и наоборот.

> (define (list->stream L)

(if (null? L) empty-stream

(cons-stream (car L) (list->stream (cdr L)))))

> (define (stream->list S)

(if (empty-stream? S) nil

(cons (head S) (stream->list (tail S)))))

Примеры.

> (list->stream '(1 2 3))

(1 . #<promise:...)

> (stream->list s_abc)

(a b c)

> (stream->list (list->stream '(1 2 3)))

(1 2 3)

Лучше, когда есть функция вывода значений элементов потока на дисплей

> (define (display-stream S)

(cond ((empty-stream? S) 'done)

(else (begin (display (head S))

(display "; ")

(display-stream (tail S))))))

> (define s345 (list->stream '(3 4 5)))

> (display-stream s345)

3; 4; 5;

done

Процедуры для потоков совершенно аналогичны процедурам для списков. Единственное, что требуется изменить - имена конструкторов и селекторов.

Вот процедура сложения двух числовых потоков

> (define (stream-add s1 s2)

(if (or (empty-stream? s1)

(empty-stream? s2)) empty-stream

(cons-stream (+ (head s1) (head s2))

(stream-add (tail s1) (tail s2))) ))

> (display-stream (stream-add s345 s345))

6; 8; 10;

done

Вместо перечисления всех элементов при задании потока, можно, например, написать генерацию целочисленного потока в указанном диапазоне.

> (define (stream-range m n)

(if (> m n) empty-stream

(cons-stream m

(stream-range (+ m 1) n))))

> (stream->list (stream-range 6 10))

(6 7 8 9 10)

> (elem-no 10 (stream-range 10 1000000))

19

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

Сначала вызывается функция stream-range, которая возвращает пару

(1 . "обещание вычислить (stream-range 10 1000000)").

Затем  elem-no  пытается вычислить хвост этой пары, что приводит к новому вызову stream-range, которая теперь уже возвращает пару

(2 . "обещание вычислить (stream-range 11 1000000)").

И т.д. Процесс продолжается, пока не будет получена пара

(10 . "обещание вычислить (stream-range 19 1000000)").

Поскольку именно этот элемент и был нужен при обращении к функции elem-no, на этом она завершит свою работу и больше не будет требовать выполнения обещаний. Таким образом, остаток потока никогда не будет вычислен, что сэкономит массу времени и пространства.

Для сравнения затраченного времени приведем два примера работы DrSheme для получения конкретного числа

  • Сначала в среде списков (затрата по памяти около 110Мб)

Создаём необходимую для сравнения «неправильную» функцию потоков

> (define (cons-stream a b) (cons a (delay b)))

> (time (elem-no 100000 (stream-range 10 1000000)))

cpu time: 7391 real time: 7391 gc time: 5341

100009

  • И теперь в среде потоков

Вспоминаем ПРАВИЛЬНУЮ функцию для потоков

> (define-syntax cons-stream

(syntax-rules () ((_ a b) (cons a (delay b))) ))

> (time (elem-no 100000 (stream-range 10 1000000)))

cpu time: 359 real time: 359 gc time: 266

100009

Итак, на примере реализации лисповских потоков мы разобрали методику оптимизации работы с большими последовательностями данных, известную как метод меморизации или табулирование функций.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]