Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

КН Лекции 1-9

.pdf
Скачиваний:
16
Добавлен:
23.02.2015
Размер:
2.64 Mб
Скачать

Алгоритмический анализ. Лекция 7.

Программирование на Scheme

§ 1. Основы языка Scheme

Автор языка Лисп – профессор математики и философии Джон МакКарти, выдающийся ученый в области искусственного интеллекта. Он предложил проект языка Лисп, идеи которого возбудили не утихающие до наших дней дискуссии о сущности программирования. Сформулированная Джоном Мак-Каpти (1958) концепция символьной обработки информации восходит к идеям Чѐрча (лямбда-исчисление) и других видных математиков конца 20-х годов предыдущего века. Джон Мак-Карти предложил функции рассматривать как общее понятие, к которому могут быть сведены все другие понятия программирования. Lisp стал первым функциональным языком программирования. Из действующих на данный момент языков более старым является лишь

FORTRAN.

Из-за всѐ более возрастающей сложности программного обеспечения всѐ большую роль начинает играть типизация. В конце 70-х — начале 80-х годов XX века интенсивно разрабатываются модели типизации, подходящие для функциональных языков. Большинство этих моделей включали в себя поддержку таких мощных механизмов как абстракция данных и полиморфизм. Появляется множество типизированных функциональных языков: ML, Scheme, Hope, Miranda, Clean, Haskell и

многие другие. Вдобавок постоянно увеличивается число диалектов.

Свойства функциональных языков

Перечислим основные свойства функциональных языков программирования:

краткость и простота;

строгая типизация;

модульность;

функции — это значения;

чистота (отсутствие побочных эффектов);

отложенные (ленивые) вычисления.

Scheme. Диалект Lisp’а, предназначенный для научных исследований в области computer science. При разработке Scheme был сделан упор на

элегантность и простоту языка. Благодаря этому язык получился намного меньше, чем Common Lisp.

Выражения.

(* 5 99)

495

(/ 10 5)

2

(+ 2.7 10)

12.7

пример

(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))

интерпретатор вычисляет его и дает ответ 57. Мы можем облегчить себе задачу, записывая такие выражения в форме

(+ (* 3 (+ (* 2 4)

(+ 3 5))) (+ (- 10 7)

6))

Эти правила форматирования называются красивая печать (pretty printing). Согласно им, всякая длинная комбинация записывается так, чтобы ее операнды выравнивались вертикально. Получающиеся отступы ясно показывают структуру выражения.

Переменная задаѐтся следующей конструкцией языка: (define имя <первоначальное значение>)

Пример:

(define width 3) (define height 7)

(* 2 (+ width height))

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

(define a 3) (define b 4)

(+ (* a a) (* b b))

Возможен и другой вариант. Определим дополнительную функцию square.

(define (square x) (* x x))

Это можно понимать так:

Чтобы возвести в квадрат что-л. умножь это само на себя. Здесь мы имеем составную процедуру (compound procedure), которой мы дали имя square.

(+ (square a) (square b))

Общий формат описания функций

(define (название параметр параметр …) тело_функции)

Функция возвращает последнее вычисленное значение. Это означает, что следующая функция square2:

(define (square2 x) (* 2 2) (* x x))

вернѐт тот же результат, что и square, перед этим умножив два на два безо всякого эффекта. Перепишем пример с суммой квадратов чисел заново:

(define a 3) (define b 4)

(define (square x) (* x x)) (+ (square a) (square b))

Нам не хватало слов в языке — мы их добавили. Вообще, когда пишете программу на Лиспе, вы описываете не алгоритм, а сначала создаѐте язык, а потом на нѐм формулируете исходную задачу. Несколько точнее — вы «подгоняете» данный вам язык Scheme до тех пор, пока он не станет совпадать с языком, на котором задача формулируется легко.

Пример.

(define (sum-of-squares x y) (+ (square x) (square y))) (sum-of-squares 3 4)

25

(sum-of-squares (+ 5 1) (* 5 2))

(+ (square (+ 5 1)) (square (* 5 2)) )

(+ (* (+ 5 1) (+ 5 1)) (* (* 5 2) (* 5 2)))

за которыми последуют редукции

(+ (* 6 6) (* 10 10)) (+ 36 100)

136

В Лиспе используется аппликативный порядок вычислений, отчасти из-за дополнительной эффективности, которую дает возможность не вычислять многократно выражения вроде приведенных выше (+ 5 1) и (* 5 2), а отчасти, что важнее, потому что с нормальным порядком вычислений становится очень сложно обращаться, как только мы покидаем область процедур, которые можно смоделировать с помощью подстановки.

Пример.

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

Scheme предоставляет нам несколько готовых «глаголов»: read

для чтения имени, display

вывод чего-то на дисплее, newline

вывод перевода строки.

Мы бы хотели иметь такие «глаголы»:

privet

для приветствия с одним параметром — именем пользователя; polzovatel

для получения имени пользователя, без параметров. Наша задача выглядела бы так:

(privet (polzovatel))

Дело за малым — определить privet и polzovatel. Определим их как пару функций.

Вот полный текст программы.

(define (privet imja) (display "Privet ") (display imja)

(display "!") (newline))

(define (polzovatel) (write "Predstavtes:") (read))

(privet (polzovatel))

Лисп — полноценный функциональный язык, а поэтому функции — полноправные члены этого языка, независимо от того, определили вы их сами, или они уже были в языке готовые. В частности, их можно передавать в качестве параметров в другие функции, а там уже делать с ними всѐ, что потребуется. Например, функцию «модуль числа» можно определить так:

(define (abs x) (if (positive? x )

x

(- x)))

«Определим, что функция abs возвращает свой аргумент, если он положителен, иначе — -x». А можно и так:

(define (abs x)

((if (positive? x) + -) x))

«…если аргумент положителен, то плюс, иначе минус x». Здесь в результате исполнения выражения if возвращается функция + или -, которая затем применяется к аргументу x. Полагаю, что смысл конструкции if вам сразу ясен. Сначала проверяется первый аргумент, если он истинен, то исполняется второй аргумент, иначе третий. Общий формат таков:

(if условие

<действие, если условие выполняется> <действие в противном случае>)

Общая форма условного выражения такова:

(cond (условие1 действие1) (условие2 действие2)

...

(условиеN действиеN))

(define (abs x) (cond ((> x 0) x)

((= x 0) 0)

((< x 0) (- x)))) (define (abs x)

(cond ((< x 0) (- x)) (else x)))

В качестве примера рассмотрим задачу вычисления квадратного корня. Мы можем определить функцию «квадратный корень» так:

√x = такое y, что y ≥ 0 и y^2 = x

Это описывает совершенно нормальную математическую функцию, но это декларативный стиль, не объясняющий, как вычислять корень.

Как же вычисляются квадратные корни? Наиболее часто применяется метод Ньютона (последовательных приближений), который основан на том, что имея некоторое неточное значение y для квадратного корня из числа x, мы можем с помощью простой манипуляции получить более

точное значение (более близкое к настоящему квадратному корню), если возьмем среднее между y и x/y.

Запишем эту базовую стратегию в виде процедуры:

(define (sqrt-iter y x)

(if (good-enough? y x) y

(sqrt-iter (improve y x) x)))

Значение приближения улучшается с помощью взятия среднего между ним и частным подкоренного числа и старого значения приближения:

(define (improve y x) (average y (/ x y)))

где

(define (average x y) (/ (+ x y) 2))

Будем улучшать приближения до тех пор, пока его квадрат не совпадет с подкоренным числом в пределах заранее заданного допуска

(здесь 0.001):

(define (good-enough? y x) (< (abs (- (sqr y) x)) 0.001))

Мы можем для начала предполагать, что квадратный корень любого числа равен 1:

(define (sqrt x) (sqrt-iter 1.0 x))

http://www.plt-scheme.org/software/drscheme/ - интерпретатор и среда выполнения для языка Scheme.

В настоящее время изменилось название и адрес сайта: http://racket-lang.org/

Алгоритмический анализ. Лекция 8.

Программирование на Scheme

§ 1. Основы языка Scheme (продолжение)

Повторим основные возможности языка Scheme. Переменные:

(define имя <первоначальное значение>)

Описание функций:

(define (название параметр параметр …) тело_функции)

Управляющие конструкции:

(if условие

<действие, если условие выполняется> [<действие в противном случае>])

Общая форма условного выражения:

(cond

(<условие 1> <действие 1>) (<условие 1> <действие 1>)

. . .

(<условие 1> <действие 1>)

[(else <альтернативное действие>)])

На прошлой лекции мы остановились на итерационном вычислении √x

(define (sqrt-iter x) (iter 1.0 x))

(define (iter y x)

(if (good-enough? y x) y

(iter (improve y x) x))) (define (good-enough? y x)

(< (abs (- (square y) x)) 0.001)) (define (improve y x)

(average y (/ x y)))

(sqrt-iter 2) 1.4142156862745097

Упражнение (1.6 из SICP).

Определим новую версию if:

(define (new-if predicate then-clause else-clause) (cond (predicate then-clause)

(else else-clause)))

Протестируем его возможности: (new-if (= 2 3) 0 5)

5

(new-if (= 1 1) 0 5)

0

Все работает правильно.

Перепишем через new-if программу вычисления квадратного корня:

(define (iter y x)

(new-if (good-enough? x y) y

(iter (improve x y) x)))

Что получится, когда мы попытаемся использовать эту процедуру для вычисления квадратных корней?

При вычислении new-if сначала должны быть вычислены все ее аргументы. С первыми двумя из них проблем нет, а вот при вычислении третьего new-if снова обращается к iter, которая затем опять вызовет new-if… и так до бесконечности. Ограничивающего условия,

обеспечивающего выход из этого порочного круга, нет. Таким образом программа, переписанная в таком виде, зациклится. Получили бесконечную рекурсию.

Почему же такой проблемы не возникает при использовании нормального if вместо new-if? Обычный if является особой формой, он вычисляется не так, как стандартные процедуры. Для вычисления его значения не обязательно будут вычислены все операнды (более того, гарантированно будут вычислены только два из трех). Таким образом, iter будет вычисляться только в случае, когда решение еще не достаточно хорошее, а в противном случае вычисляться не будет, что и гарантирует выход из цикла. Другими словами, if использует ленивые вычисления.

Вернемся к нашему примеру. Сравним два варианта описаний:

Здесь плохо то, что много лишних функций видно наружу, и x везде просочился. Используем внутренние define’ы.

Старая версия:

(define (sqrt-iter x) (iter 1 x))

(define (iter y x)

(if (good-enough? y x) y

(iter (improve y x) x))) (define (good-enough? y x)

(< (abs (- (sqr y) x)) 0.001)) (define (improve y x)

(/ (+ y (/ x y)) 2))

Новая версия:

(define (sqrt-iter x)

(define (good-enough? y)