Вы находитесь на странице: 1из 23

Лекция 8.

Эффективный ML

Введение в Функциональное программирование

Джон Харрисон

Университет Кембриджа

10 сентября 2008 г.

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Темы

Использование стандартных комбинаторов


Хвостовая рекурсия и аккумуляторы
Принудительное вычисление
Минимизация операций cons
Исключения
Ссылки и массивы

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Использование стандартных комбинаторов

Часто получается, что комбинаторы оказываются очень полезными:


используя их, мы можем реализовать практически всё что угодно.
Это правда, потому что у нас есть функции высшего порядка.
Например, функцию itlist:

itlist f [x1 ; x2 ; . . . ; xn ] b
= f x1 (f x2 (f x3 (· · · (f xn b))))

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


определений функций над списками. Мы определяем её как:
#l e t r e c i t l i s t f =
fun [ ] b −> b
| ( h : : t ) b −> f h ( i t l i s t f t b ) ; ;
i t l i s t : ( ’ a −> ’ b −> ’ b ) −>
’ a l i s t −> ’ b −> ’ b = <fun>

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Примеры Itlist (1)


Например, вот функция для суммирования всех элементов списка:
#l e t sum l =
i t l i s t ( fun x sum −> x + sum ) l 0 ; ;
sum : i n t l i s t −> i n t = <fun>
#sum [ 1 ; 2 ; 3 ; 4 ; 5 ] ; ;
− : i n t = 15
#sum [ ] ; ;
− : int = 0
#sum [ 1 ; 1 ; 1 ; 1 ] ; ;
− : int = 4

По другому можно записать просто:


#l e t sum l = i t l i s t ( p r e f i x +) l 0 ; ;

Если же мы хотим перемножить элементы, то мы поменяем


определение на:
#l e t sum l = i t l i s t ( p r e f i x ∗ ) l 1 ; ;

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Примеры Itlist (2)


Вот функция по отбору элементов:
#l e t f i l t e r p l =
i t l i s t ( fun x s −> i f p x then x : : s
else s ) l [ ] ; ;

а вот логические операции над списками:


#l e t f o r a l l p l =
i t l i s t ( fun h a −> p ( h ) & a ) l t r u e ; ;
#l e t e x i s t s p l =
i t l i s t ( fun h a −> p ( h ) o r a ) l f a l s e ; ;

и несколько альтернативных определений уже знакомых функций:


#l e t l e n g t h l =
i t l i s t ( fun x s −> s + 1 ) l 0 ; ;
#l e t append l m =
i t l i s t ( fun h t −> h : : t ) l m ; ;
#l e t map f l =
i t l i s t ( fun x s −> ( f x ) : : s ) l [ ] ; ;
Джон Харрисон Введение в Функциональное программирование
Лекция 8. Эффективный ML

Примеры Itlist (3)


Мы можем реализовать операции над множествами, используя эти
комбинаторы как строительные блоки.
#l e t mem x l =
e x i s t s ( fun y −> y = x ) l ; ;
#l e t i n s e r t x l =
i f mem x l then l e l s e x : : l ; ;
#l e t u n i o n l 1 l 2 =
i t l i s t insert l1 l2 ; ;
#l e t s e t i f y l =
union l [ ] ; ;
#l e t Union l =
i t l i s t union l [ ] ; ;
#l e t i n t e r s e c t l 1 l 2 =
f i l t e r ( fun x −> mem x l 2 ) l 1 ; ;
#l e t s u b t r a c t l 1 l 2 =
f i l t e r ( fun x −> n o t mem x l 2 ) l 1 ; ;
#l e t s u b s e t l 1 l 2 =
f o r a l l ( fun t −> mem t l 2 ) l 1 ; ;
Джон Харрисон Введение в Функциональное программирование
Лекция 8. Эффективный ML

Сохранение локальных переменных

Вспомним наше определение факториала:


#l e t r e c f a c t n = i f n = 0 then 1
else n ∗ fact (n − 1 ) ; ;

Вызов fact 6 порождает другой вызов fact 5 (и так далее), но


вычислителю нужно сохранять старое значение 6, чтобы в
дальнейшем произвести умножение.
Поэтому, локальные переменные функции, в нашем случае n, не
могут храниться в фиксированном месте, потому что при каждом
вызове функции нужны выделенные копии переменных.
Вместо этого, при каждом вызове функции выделяется место в
стеке.
Этот способ в ML схож с другими языками, которые поддерживают
рекурсию, включая C.

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Стек
Вот воображаемый снимок стека во время вычисления последнего
вызова fact, т.е. во время вычисления fact 0:

n=6
n=5

n=4
n=3
n=2

n=1
n=0
SP -
Заметим, что используется порядка n фреймов стека, когда у нас n
вложенных рекурсивных вызовов. Во многих ситуациях это очень
расточительно.
Джон Харрисон Введение в Функциональное программирование
Лекция 8. Эффективный ML

Хвостовая рекурсия
Теперь, в противопоставление старому определению, рассмотрим
следующее определение функции факториала:
#l e t r e c t f a c t x n =
i f n = 0 then x
else tfact (x ∗ n) (n − 1 ) ; ;
t f a c t : i n t −> i n t −> i n t = <fun>
#l e t f a c t n = t f a c t 1 n ; ;
f a c t : i n t −> i n t = <fun>
#f a c t 6 ; ;
− : i n t = 720

Рекурсивный вызов – итоговое выражение, после него никаких


вычислений не происходит; оно не является подвыражением, частью
другого выражения.
Подобные вызовы называются хвостовыми вызовами, потому что
самые последние в теле функции.
А функция, в которой все рекурсивные вызовы суть хвостовые,
называется функцией с хвостовой рекурсией.
Джон Харрисон Введение в Функциональное программирование
Лекция 8. Эффективный ML

Зачем нужна хвостовая рекурсия?

Компилятор достаточно умён, чтобы понять, что для функций с


хвостовой рекурсией можно использовать одно и тоже место в
памяти для хранения локальных переменных при рекурсивных
вызовах.
Мы избежим переполнения стека.
Дополнительный аргумент x функции tfact называется
аккумулятором, потому что он накапливает промежуточные
результаты по ходу рекурсивных вызовов и, в конце концов,
возвращается как значение функции.
Определение функций таким образом, является общепринятым
способом написания функций с хвостовой рекурсией.
В этом смысле, реализация функции с хвостовой рекурсией,
использующая аккумуляторы соответствует итеративной версии с
использованием присваиваний и while-циклов.
Единственная разница это то что мы явно передаём состояние.

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Принудительное вычисление (1)

Вспомним что ML не вычисляет выражения под λ-абстракциями.


Поэтому иногда выгодно вывести выражения за пределы
λ-абстракции, когда они не зависят от значения связанной
переменной.
Например, мы можем написать эффективную функцию для
вычисления факториала, сделав tfact локальной.
#l e t f a c t n =
l e t rec t f a c t x n =
i f n = 0 then x
e l s e t f a c t ( x ∗ n ) ( n − 1) in
tfact 1 n ;;

Однако, let-выражение рекурсивной функции tfact не вычисляется


пока fact не получит свои аргументы.
Более того, оно вычисляется каждый раз даже если не зависит от n.

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Принудительное вычисление (2)

Лучше написать следующим образом:


#l e t f a c t =
l e t rec t f a c t x n =
i f n = 0 then x
e l s e t f a c t ( x ∗ n ) ( n − 1) in
tfact 1;;

При заданном аргументе 6, этот код примерно на 20% быстрее. В


случаях же когда подвыражения приводят к б’ольшим вычислениям,
то разница может быть впечатляющей.
Большинство компиляторов автоматически не оптимизируют код
таким образом.
Эта проблема подпадает под основное направление большой научной
области — частичных вычислений.

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Минимизация операций cons (1)

Память, используемая конструкторами типа («cons-ячейки»), не


выделяется и не освобождается таким же простым способом как и
на стеке.
В общем случае достаточно сложно понять когда определённая
cons-ячейка занята и когда её можно использовать повторно.
Например:
let l = 1 : : [ ] in t l l ; ;

cons-ячейку можно сразу же освобождать. Однако если l был


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

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Минимизация операций cons (2)


Часто мы можем сделать программы более эффективными по
времени выполнения и по использованию ресурсов уменьшив
операции cons. Один простой приём заключается в уменьшении
использования append. Посмотрев на определение функции:
#l e t r e c append l 1 l 2 =
match l 1 with
[ ] −> l 2
| ( h : : t ) −> h : : ( append t l 2 ) ; ;

можно увидеть что она порождает n cons-ячеек, где n длина первого


аргумента. Например, наше определение реверсирования:
#l e t r e c r e v =
fun [ ] −> [ ]
| ( h : : t ) −> append ( r e v t ) [ h ] ; ;

очень не эффективно, оно порождает порядка n2 /2 cons-ячеек, где n


это длина списка.
Джон Харрисон Введение в Функциональное программирование
Лекция 8. Эффективный ML

Минимизация операций cons (3)


Лучше реализовать так:
#l e t r e v =
l e t rec r e v e r s e acc =
fun [ ] −> a c c
| ( h : : t ) −> r e v e r s e ( h : : a c c ) t i n
reverse [ ] ; ;

Это определение порождает только n cons-ячеек и имеет


дополнительное преимущество – за счёт хвостовой рекурсии
сохраняет стековую память.
Также, используя as, можно избежать операций cons при
сопоставлении с образцом, например вместо:
fun [ ] −> [ ]
| ( h : : t ) −> i f h < 0 then t e l s e h : : t ; ;

написав
fun [ ] −> [ ]
| ( h : : t as l ) −> i f h < 0 then t e l s e l ; ;
Джон Харрисон Введение в Функциональное программирование
Лекция 8. Эффективный ML

Исключения (1)
Все ошибки ML, например неудавшиеся сопоставления или деление
на ноль, сигнализируются посредством распространяющихся
исключений:
#1 / 0 ; ;
Uncaught e x c e p t i o n : D i v i s i o n _ b y _ z e r o

Во всех этих случаях компилятор сообщает о «необработанном


исключении». Название предполагает, что исключения можно
«обрабатывать».
Для исключений есть тип exn, который фактически является
рекурсивным типом. В отличии от обычных типов, для типа exn
допустимо вводить конструкторы в любом месте программы,
используя объявления исключений, например:
#e x c e p t i o n Died ; ;
E x c e p t i o n Died d e f i n e d .
#e x c e p t i o n F a i l e d o f s t r i n g ; ;
Exception Failed defined .

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Исключения (2)

Можно явно вызывать исключения используя конструктор raise,


например:
#r a i s e ( F a i l e d " I ␣ don ’ t ␣know␣why" ) ; ;
Uncaught e x c e p t i o n :
F a i l e d " I ␣ don ’ t ␣know␣why"

Мы можем создать своё собственное исключение на случай попытки


взять первый элемент в пустом списке:
#e x c e p t i o n Head_of_empty ; ;
E x c e p t i o n Head_of_empty d e f i n e d .
#l e t hd = fun [ ] −> r a i s e Head_of_empty
| ( h : : t ) −> h ; ;
hd : ’ a l i s t −> ’ a = <fun>

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Исключения (3)

Можно перехватывать и обрабатывать исключения с помощью


конструкции try ...with, сопровождаемой вариантами
исключений, например:
#l e t h e a d s t r i n g s l =
t r y hd s l
with Head_of_empty −> " "
| F a i l e d s −> " F a i l u r e ␣ b e c a u s e ␣ "^ s ; ;
h e a d s t r i n g : s t r i n g l i s t −> s t r i n g = <fun>
#h e a d s t r i n g [ " h i " ; " t h e r e " ] ; ;
− : string = " hi "
#h e a d s t r i n g [ ] ; ;
− : s t r i n g = ""

Есть мнение, что исключения не совсем императивная возможность.


Мы можем представить себе, что тип исключений скрыто
добавляется к типам возвращаемых значений каждой функции.
В любом случае, часто исключения весьма полезны!

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Ссылки (1)

В ML есть настоящие переменные, к которым можно присваивать, и


выражения могут в качестве побочного эффекта изменять значения
этих переменных.
Доступ к переменным осуществляется с помощью ссылок
(указателей, говоря языком C), и ссылки в свою очередь
рассматриваются в ML как обычные значения.
Чтобы создать новую ячейку памяти и инициализировать её
значением x нужно записать ref x. Это выражение возвращает
соответствующую ссылку, т.е. указатель на вновь созданную ячейку.
Изменяют содержимое ячейки через указатель.
Этот подход довольно похож на использование указателей в C:
например когда требуется использовать «изменяемые параметры» –
параметры, для которых допустимо изменение значений после
вычисления функции, используется передача параметров по
указателю.
Содержимое ссылки извлекается с помощью оператора
разыменования !. А чтобы изменить значение, используется :=.

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Ссылки (2)
Вот пример создания и использования ссылки:
#l e t x = r e f 1 ; ;
x : int ref = ref 1
#!x ; ;
− : int = 1
#x := 2 ; ;
− : unit = ()
#!x ; ;
− : int = 2
#x := ! x + ! x ; ;
− : unit = ()
#x ; ;
− : int ref = ref 4
#!x ; ;
− : int = 4

Во многих отношениях ref ведёт себя подобно конструктору типа, а


значит, может использоваться в сопоставлении с образцом.
Джон Харрисон Введение в Функциональное программирование
Лекция 8. Эффективный ML

Массивы (1)

Кроме отдельных ячеек, в ML можно использовать массивы, в


CAML они называются векторами.
Массив размера n, где каждый элемент проинициализирован
значением x создаётся следующим вызовом:
#make_vect n x ; ;

Можно прочесть элемент m вектора v с помощью:


#v e c t _ i t e m v m ; ;

а записать значение y в m-й элемент v:


#v e c t _ a s s i g n v m y ; ;

Элементы массива нумеруются с нуля. То есть элементы вектора


размера n нумеруются 0, . . . , n − 1.

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Массивы (2)
Вот простой пример:
#l e t v = make_vect 5 0 ; ;
v : int vect = [ | 0 ; 0; 0; 0; 0 | ]
#v e c t _ i t e m v 1 ; ;
− : int = 0
#v e c t _ a s s i g n v 1 1 0 ; ;
− : unit = ()
#v ; ;
− : i n t vect = [ | 0 ; 10; 0; 0; 0 | ]
#v e c t _ i t e m v 1 ; ;
− : i n t = 10

Все операции чтения и записи элементов сопровождаются контролем


границ, например:
#v e c t _ i t e m v 5 ; ;
Uncaught e x c e p t i o n :
Invalid_argument " vect_item "

Джон Харрисон Введение в Функциональное программирование


Лекция 8. Эффективный ML

Императивные возможности и типы

Есть печальная нестыковка между ссылками и let-полиморфизмом.


Например, согласно обычным правилам let-полиморфизма,
следующее выражение должно быть правильным, даже если оно
записывает нечто как объект типа int, а затем читает как объект
типа bool.
#l e t l = r e f [ ] ; ;
l : ’_a l i s t r e f = r e f [ ]
#l := [ 1 ] ; ;
#hd ( ! l ) = t r u e ; ;

ML накладывает ограничения на полиморфный тип выражений,


которые содержат ссылки. Подчёркивание перед типом переменной
обозначает что l не полиморфна в привычном смысле; скорее она
имеет один фиксированный тип, хотя тип этот всё ещё неопределён.

Джон Харрисон Введение в Функциональное программирование

Вам также может понравиться