lectures fp

91
Гл. ас. д-р Веселка Боева Ръководство за лабораторни упражнения ВЪВЕДЕНИЕ В СТАНДАРТА ML в рамките на учебната дисциплина Функционално ПрограмиранеТЕХНИЧЕСКИ УНИВЕРСИТЕТ - СОФИЯ ФИЛИАЛ ПЛОВДИВ 2003

Upload: ivelina-mihaleva

Post on 08-Mar-2015

220 views

Category:

Documents


11 download

TRANSCRIPT

Page 1: Lectures FP

Гл. ас. д-р Веселка Боева

Ръководство за лабораторни упражнения

ВЪВЕДЕНИЕ В СТАНДАРТА ML

в рамките на учебната дисциплина “Функционално Програмиране”

ТЕХНИЧЕСКИ УНИВЕРСИТЕТ - СОФИЯ ФИЛИАЛ ПЛОВДИВ

2003

Page 2: Lectures FP

Настоящето ръководството е съобразено с учебната програма на дисциплината

Функционално програмиране от специалност “Компютърни системи и

технологии” на Технически университет София, филиал Пловдив.

Функционалното програмиране има за цел запознаване с основните принципи

залегнали в езиците за функционално програмиране и изграждането на

теоретичната основа и възможности за сравняване на различните стилове за

програмиране. В ръководство са включени теми свързани с изграждането на

практически умения за работа с функционалния език SML (Standard ML).

Разгледани са и някои теоритични принципи запознаващи с основните

харектеристики на функционалното програмиране, но основно е обърнато

внимание на въпросите за създаването на ефективни алгоритми и

практическото програмиране в термините на SML.

Ръководството може да се използва и от студенти от други висши учебни

заведения и колежи при обучението им по функционално програмиране, както

и в други сродни учебни дисциплини като например, Структури от данни,

Синтез и анализ на алгоритми и Програмни езици.

Автор: гл. ас. д-р Веселка Боева

Рецензенти: проф. дтн Стойчо Стойчев, доц. д-р Георги Тотков

Page 3: Lectures FP

Въведение в стандарта ML

Функционално програмиране

3

СЪДЪРЖАНИЕ

Предговор 5

Лабораторно упражнение No 1 7

Увод в програмирането на ML. Основни изрази, стойности и

типове. Привързвания и декларации.

Лабораторно упражнение No 2 13

Съставни типове: наредени n-торки и записи. Съпоставяне на

образци. Полиморфни функции.

Лабораторно упражнение No 3 21

Рекурсивни дефиниции на функции. Локални декларации.

Лабораторно упражнение No 4 29

Списъци. Функции над списъци.

Лабораторно упражнение No 5 39

Приложение на списъци: бинарна аритметика, матрици,

множества.

Лабораторно упражнение No 6 47

Графови алгоритми и алгоритми за сортиране на списъци.

Лабораторно упражнение No 7 57

Дефиниране на потребителски типове – type и datatype

декларации. Изключения. Дървета.

Page 4: Lectures FP

Въведение в стандарта ML

Функционално програмиране

4

Лабораторно упражнение No 8 67

Двоични търсещи дървета. Функционални масиви и

приоритетни опашки.

Лабораторно упражнение No 9 75

Функции от по-висок ред. Безкрайни списъци и отложени

пресмятания.

Лабораторно упражнение No 10 83

Абстрактни типове данни

Литература 91

Page 5: Lectures FP

Въведение в стандарта ML

Функционално програмиране

5

ПРЕДГОВОР Функционалните езици са базирани върху понятието на математическите

функции, които при даден списък от действителни параметри връщат единична

стойност съгласно някакво правило. Чистите функционални езици не допускат

странични ефекти, т.е. стойностите на параметрите не се променят по време на

функционалното извикване. Така, във функционалните езици програмата е

функционално извикване с параметри, които е възможно да извикват други

функции за да произведат действителните параметри. В практиката има само

няколко чисти функционални езици, тъй като основните странични ефекти като

входа и изхода са необходими.

Отбелязваме, че функционалните езици спадат към групата на

декларативните езици. Езиците за програмиране се разделят на две големи

групи: императивни и декларативни. Императивните езици специфицират как

изчислението се извършва, докато декларативните специфицират какво трябва

да се изчисли. Когато програмираме в декларативен стил не правим

присвоявания на програмните променливи. Интерпретатора или компилатора за

конкретния език управлява паметта вместо нас. Така декларативните езици са

на “по-високо ниво” отколкото императивните езици, т.е. програмиста оперира

по-отдалечено от компютър.

Първият и най-известен функционален език е LISP (List Processing). Той е

език със специално предназначение, а именно бил е създаден за нуждите на

изкуствения интелект. LISP e базиран върху λ-смятането, разработено от Alonzo

Church за да формализира интуитивните понятия относно функциите. Други

функционални езици базирани върху λ-смятането са SASL, KRC, Haskell и

Miranda. Те са вероятно единствените комерсиално разпространявани

функционални езици. Втора група функционални езици са базирани повече

върху общи математически понятия отколкото върху λ-смятането. Първият

такъв език е APL. Фундаменталният тип данни на LISP е списък, докато на APL

Page 6: Lectures FP

Въведение в стандарта ML

Функционално програмиране

6

е масив със съответните операции. Модерни езици повлияни от APL са FP и

ML.

Настоящето ръководство разглежда теми свързани с изграждането на

практически умения за работа с функционалния език ML. Първият ML

компилатор е построен през 1977 в Edinburgh. Името “ML” е съкратено от Meta

Language. Много диалекти се появяват след това, но както се е случило и с LISP

бил e разработен общ език SML (Standard ML). SML е резултат от

стандартизиращите усилия и свързването на езиците ML и HOPE. Той прилича

на LISP в много отношения, но има и много предимства. Например,

съпоставянето на образци елиминира нуждата от функции деструктори каквито

са LISP функциите CAR и CDR. Освен това SML избягва многото скоби

характерни за LISP изразите.

ML е език за функционално програмиране, в който функциите могат да

бъдат подавани като аргументи на други функции или да бъдат резултат от

тяхното прилагане, т.е. те са обекти данни от първи клас. Основният

управляващ механизъм в ML е прилагането на рекурсивни функции. Освен това

ML е интерактивен език. Всеки израз може да бъде анализиран, компилиран и

изпълнен. ML системата отпечатва стойността на израза заедно с неговия тип.

Извличането на типа на израза става автоматично. Друго предимство на езика

са неговите полиморфни типове, които предотвратяват от грешки по време на

изпълнение. За всяка функция ML извлича тип, който е максимално общ. ML е

снабден с възможността за дефиниране на потребителски абстрактни типове

данни. Нови типове заедно с множество от функции над обектите от тези

типове могат да бъдат дефинирани. Така детайлите на изпълнението са скрити

от потребителя на типа. Освен това, ML има механизъм на изключенията

(exceptions), който позволява да бъдат обработвани нестандартни или грешни

условия възникващи по време на изпълнение. Модулната система на ML е едно

от най-големите предимства на езика. Тя позволява конструирането на големи

програми чрез свързването на отделно компилирани модули.

Page 7: Lectures FP

Въведение в стандарта ML

Функционално програмиране

7

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 1

УВОД В ПРОГРАМИРАНЕТО НА ML. ОСНОВНИ ИЗРАЗИ,

СТОЙНОСТИ И ТИПОВЕ. ПРИВЪРЗВАНИЯ И ДЕКЛАРАЦИИ

1. Взаимодействие с ML системата Повечето функционални езици са интерактивни. Ако въведете израз ML

го анализира, компилира и изпълнява, и резултата се извежда на екрана. Например, може да се въведе израз следван от точка и запетая ... − 3+5; ... и ML отговаря

> val it = 8 : int ML системата отпечатва символа “−“ когато чака вход на информация, а символът “>” предшествува отговора на системата.

− 5.3 – 2.4; > val it = 2.9 : real

Всичко което се въвежда завършва с точка и запетая (;). Освен това в отговора на ML системата се съдържа както стойността на израза така и неговия тип. Така real е типът на реалните числа, а int е типът на целите.

2. Основни изрази, стойности и типове: int, real, string и bool Най-простите ML стойности са целите и реалните числа, символните

низове и булевите стойности. Отрицателните числа започват с унарен минус “˜”. Операциите допустими над целочислените величини са събиране (+), изваждане (-), умножение (*), целочислено деление (div) и остатък от целочислено деление (mod).

− ˜ 82; > val it = ˜ 82 : int − (3+6) mod 2; > val it = 1 : int Реалните константи съдържат десетична точка или експонентен маркер

(Е) или и двете. Например: 3.14159, 0.0314Е2, 314.159Е˜2. Аритметичните функции приложими над реални числа са +, -, * и деление (/). Други функции

Page 8: Lectures FP

Въведение в стандарта ML

Функционално програмиране

8

дефинирани над реални величини са квадратен корен (sqrt), синус (sin), косинус (cos), арктангенс (arctan), експонента (exp) и натурален логаритъм (ln).

ML прави разлика между цели и реални числа, т.е. те не могат да бъдат смесвани. Аритметичните операции +, -, * са дефинирани и за двата типа, но не е позволено например, реално число да бъде събирано с цяло. Функцията real(i) превръща цялото число i в съответното реално, а функцията floor(r) закръглява реалното число r до най-голямото цяло число по-малко от r. Така, функциите real и floor са необходими когато цели и реални числа участвуват в един и същи израз.

− real (3); > val it = 3.0 : real − sqrt (real(19 mod 5)); > val it = 2.0 : real − floor (3.3); > val it = 3 : int − floor (˜3.3); > val it = ˜4 : int

Абсолютната стойност на число x се записва abs(x) независимо дали е цяло или реално.

Типът string определя множество от крайни последователности от символи. Константите от този тип се поставят в кавички.

− "rain"; > val it = "rain" : string

Операторът за конкатинация (^) съединява два низа, а функцията size връща броя на символите в низа.

− "it " ^ "is raining"; > val it = "it is raining" : string − size "it is raining"; > val it = 13 : int

В ML, има две функции ord и chr за преминаване между символи (низове с дължина едно) и техните кодове. Ако цялото число k е в интервала [0, 255] тогава chr(k) дава символа с код k. Обратно, ако s е символ то ord(s) е неговият код.

− chr 65; > val it = "A" : string

Page 9: Lectures FP

Въведение в стандарта ML

Функционално програмиране

9

− ord "a"; > val it = 97 : int Типът bool съдържа двете стойности true и false. Очевидно е че,

сравненията се явяват изрази от булев тип. В ML oператорите за сравнение <, <=, >, >=, = и <> са дефинирани не само за целите и реалните числа, но и за низовете.

− (3.0+2.0) = real (3+2); > val it = true : bool − "a13" > "a122"; > val it = true : bool

Сравненията могат да бъдат комбинирани чрез булевите операции. В ML булевите операциите са логическо отрицание (not), логическо или (orelse) и логическо и (andalso).

− (3.0+2.0) = real (3+2) andalso "a13" > "a122"; > val it = true : bool Да разгледаме условния израз if e then e1 else e2 . Стойността му зависи от

израза e, който е от булев тип. Отбелязваме, че else клаузата не може да се изпуска.

− if 4*5 mod 3 = 1 then 1 else 0; > val it = 0 : int

3. Привързвания и декларации В ML декларациите служат за въвеждане на имена (идентификатори).

Всички идентификатори трябва да бъдат декларирани преди тяхната употреба. Да предположим, че искаме да изчислим лицето на кръг с радиус r по формулата area = πr². Следователно, започваме с декларирането на имената π и r.

− val pi = 3.14159; > val pi = 3.14159 : real − val r = 3.0; > val r = 3.0 : real

Така променливите pi и r са въведени чрез привързването им към стойностите 3.14159 и 3.0, съответно. Тези променливи сега са валидни в израза:

− pi * r * r; > val it = 28.27431 : real

Page 10: Lectures FP

Въведение в стандарта ML

Функционално програмиране

10

ML пази стойността на последно въведения израз в идентификатора it. Тази стойност може да бъде използвана при следващи изчисления чрез обръщение към it.

− val area = it; > val area = 28.27431 : real Функциите в ML могат да бъдат дефинирани почти по същия начин както

в математиката. Така формулата за лицето на кръг може да бъде описана като функция в ML както следва:

− fun area (r) = pi*r*r; > val area = fn : real −> real Функционалните декларации започват с ключовата дума fun. В по-горе

дефинираната функция area е името на функцията, r е формалният параметър и pi*r*r е тялото. При декларирането на функции отново ML системата връща информация за стойността и типа им. Типът на функцията area е real → real, което означава, че функцията взема реално число като аргумент и връща реално число като резултат. Стойността на функцията се отпечатва като fn. В ML, както в повечето функционални езици с изключение на LISP, функциите са абстрактни стойности, които не могат да бъдат изследвани. Нека сега да извикаме функцията area за да изчислим лицето на кръг с радиус 2.0.

− area (2.0); > val it = 12.56636 : real

Отбелязваме, че скобите във функционалните дефиниции не са задължителни, т.е. дефиницията на функцията area

− fun area r = pi*r*r; е еквивалентна на предишната.

За разлика от променливите в процедурните езици тези във функционалните езици не могат да бъдат обновявани. Ако име се декларира отново тогава новото значение се възприема, но това не оказва влияние върху съществуващите вече употреби на името.

− val pi = 0.0; > val pi = 0.0 : real − area (3.0); > val it = 28.27431 : real

Очевидно area се прилага над първоначалната стойност на pi. ML може да извлече типовете на повечето изрази от типовете на

функциите и константите участващи в тях. Обаче, някои стандартни функции

Page 11: Lectures FP

Въведение в стандарта ML

Функционално програмиране

11

имат повече от едно значение, т.е. те са пренатоварени (overloaded). Например, функцията * е дефинирана и за целите и за реалните числа, т.е. означава две различни функции. Типът на такива функции се определя от контекстта. Понякога обаче, се налага типовете да се зададат изрично. Например, ML не може да определи типа на следващата функция, която повдига на квадрат своя аргумент.

− fun sqr x = x*x; Да предположим, че функцията е замислена да работи с цели числа. Тогава, типът int може да бъде вмъкнат на няколко места във функционалната дефиниция. Може да се специфицира типа на аргумента:

− fun sqr (x : int) = x*x; > val sqr = fn : int −> int

или типа на резултата: − fun sqr x : int = x*x; > val sqr = fn : int −> int

Накрая възможно е да се заяви и типа на тялото: − fun sqr x = x*x : int; > val sqr = fn : int −> int

Функции, които връщат булева стойност се наричат предикати. Като пример да разгледаме функция, която проверява дали символен низ се сътои от една малка буква.

− fun is_letter s = "a" <= s andalso s <= "z" andalso size s = 1;

> val is_letter = fn : string −> bool Освен чрез булевите операции (andalso, orelse и not) функциите могат да бъдат описвани и като се използва оператора if. Следващата функция, която изчислява знака на цяло число, е дефинирана чрез два вложени условни оператора.

− fun sign n = if n > 0 then 1 else if n = 0 then 0 else ˜1; > val sign = fn : int −> int

Page 12: Lectures FP

Въведение в стандарта ML

Функционално програмиране

12

Задачи за упражнение

1. Дадени са цяло число i и символен низ s. Напишете булев израз на ML,

който има стойност true когато i и s образуват валидна дата. Например, 15 "May". Допускаме, че годината не е високосна.

2. Да се дефинира функция double, която удвоява цялото число i. 3. Да се дефинира функция even, която проверява дали дадено цяло число е

четно. 4. Да се дефинира функция sum, която намира сумата на първите n

естествени числа, като се използва формулата за намиране на сумата на първите n члена на аритметична прогресия.

5. Да се дефинира на ML тригонометричната функция котангенс (cotan). 6. Тригонометричните функции работят с радиани. Нека да се дефинира

версия на функцията sin, която да работи с градуси. Формулата за преобразуване на ъгъл α от градуси в радиани е απ /180˚.

7. Да се дефинира на ML функцията десетичен логаритъм като се използва стандартната функция ln.

8. Да се дефинира функция is_empty, която проверява дали даден символен низ е празен.

9. Да се дефинира функция is_digit, която проверява дали даден символен низ се състои от една цифра.

10. Да се дефинира функция to_capital, която трансформира малките букви в съответните главни в зададен символен низ.

11. Да се дефинира функция, която изчислява периметъра на окръжност с радиус r по формулата 2πr.

12. Да се дефинира функция round, която по зададено реално число връща най-близкото цяло. Например, round (1.6) да дава 2, а round (1.4) = 1.

13. Да се дефинира функция ceiling, която по зададено реално число връща най-близкото, по-голямо цяло число, т.е. ceiling (1.6) = ceiling (1.4) = 2.

Page 13: Lectures FP

Въведение в стандарта ML

Функционално програмиране

13

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 2

СЪСТАВНИ ТИПОВЕ: НАРЕДЕНИ N-ТОРКИ И ЗАПИСИ.

СЪПОСТАВЯНЕ НА ОБРАЗЦИ. ПОЛИМОРФНИ ФУНКЦИИ

1. Наредени n-торки. Функции с многочислени аргументи и резултати. Инфиксни оператори Стандартът ML снабдява с наредени двойки, тройки и т.н., n-торки. Така,

n-торка с компоненти x1, x2,…, xn се записва (x1, x2,…, xn ) и се създава чрез израз от вида (е1, е2,…, еn ), където е1, е2,…, еn са изрази. С n-торките функциите могат да се разглеждат като имащи многочислени аргументи и резултати. Отбелязваме, че компонентите на n-торките могат да бъдат също n-торки или стойности от всеки друг тип.

Нека да въведем наредена двойка от реални числа. − (2.5, ˜ 2.2); > val it = (2.5, ˜ 2.2) : real * real

Наредените двойки са стойнoсти в ML и могат да им бъдат давани имена. − val a = it; > val a = (2.5, ˜ 2.2) : real * real − val b = (8.9 - 5.7, 1.5); > val b = (3.2, 1.5) : real * real

Да разгледаме сега функция lengthvec, която изчислява дължината на вектор (x, y).

− fun lengthvec (x, y) = sqrt(x*x + y*y); > val lengthvec = fn : real * real −> real

Функцията lengthvec има тип real × real → real, т.е. нейният аргумент е наредена двойка от реални числа, а резултата и е друго реално число.

− lengthvec а; > val it = 3.33016516107 : real Наредените n-торки освен аргументи могат също да бъдат и резултати от

функции. Така следващата функция, която реализира операцията скаларно произведение на вектор с число, връща наредена двойка от реални числа като резултат.

Page 14: Lectures FP

Въведение в стандарта ML

Функционално програмиране

14

− fun scalevec (r, (x, y)) : real * real = (r*x , r*y); > val scalevec = fn : real * (real * real) −> real * real

Операцията умножение (*) е overloaded затова изрично е зададен типа на резултата в горната функция (real × real). Както споменахме компонентите на наредените n-торки могат да бъдат от различен тип. Например, аргумента на функцията scalevec е наредена двойка от реално число и наредена двойка от реални числа, т.е. real × (real × real).

Очевидно, с наредените n-торки функциите в ML могат да имат произволен брой аргументи и резултати. Функцията lengthvec оперира над реалните числа x и y, т.е. тя е функция на два аргумента. Сумата на два вектора (x1, y1 ) и (x2, y2 ) е (x1 + x2, y1 + y2). Тази функция в ML има аргумент наредена двойка от наредени двойки.

− fun addvec ((x1, y1), (x2, y2)) : real * real = (x1 + x2, y1 + y2); > val addvec = fn : (real * real) * (real * real) −> real * real

Горната функция може еквивалентно да бъде разглеждана като едноаргументна (наредена двойка от наредени двойки от реални числа), двуаргументна (две наредени двойки от реални числа) и четириаргументна (четири реални числа).

− addvec (а, b); > val it = (5.7, ˜ 0.7) : real * real − addvec (it, (0.1, 0.2)); > val it = (5.8, ˜ 0.5) : real * real

Строго говорейки всяка ML функция има един аргумент и един резултат. Инфиксният оператор е функция която се записва между двата аргумента, като например 2 + 2 = 4. Повечето функционални езици позволяват дефиниране на собствени инфиксни оператори. Нека например, да дефинираме инфиксния оператор xor, който задава логическата функция “изключващо или”. Първо се изпозва ML директивата infix.

− infix xor; Сега можем да записваме p xor q вместо xor (p, q).

− fun p xor q = (p orelse q) andalso not (p andalso q); > val xor = fn : (bool * bool) −> bool − (4*5 mod 3 = 1) xor (4*5 div 3 <> 1); > val it = true : bool Инфиксната директива в ML може да задава и приоритет от 0 до 9.

Приоритета по подразбиране е 0. Така оператора times по-долу има приоритет 7, т.е. приоритета на операцията * в ML.

Page 15: Lectures FP

Въведение в стандарта ML

Функционално програмиране

15

− infix 7 times; − fun a times b = "( " ^ a ^ "*" ^ b ^ ")"; > val times = fn : (string * string) −> string − "1" times "2" times "3"; > val it = "((1*2)*3)" : string

Отбелязваме, че 13 – 4 – 6 е еквивалентно на (13 – 4) – 6, защото минуса е присъединен наляво. Директивата infix извършва привързване наляво, докато infixr надясно.

− infixr 8 pow; − fun a pow b = "( " ^ a ^ "$" ^ b ^ ")"; > val pow = fn : (string * string) −> string − "m" times "i" pow "j" pow "3" times "n"; > val it = "((m*(i$(j$3)))*n)" : string В ML ключавата дума op отменя инфиксния статус. Например, ако

разгледаме оператора times то op times е съответната функция, която може да се приложи върху двойка аргументи.

− op times ("m", "m"); > val it = "(m*m)" : string

Освен това директивата nonfix превръща инфиксния оператор в обикновенна функция. Следващо като се използва инфиксната директива тази функция отново може да бъде превърната в инфикстнна.

− nonfix *; − *(2, 3); > val it = 6 : int

2. Съпоставяне на образци За да извършваме изчисления с наредените n-торки е необходимо да

можем да извличаме компонентите им. Съпоставянето на образци е добър начин за това. Например, функция дефинирана над образеца (x,y) се обръща към компонентите на своя аргумент чрез образците променливи x и y.

− fun min (x, y) : real = if x < y then x else y; > val min=fn : real * real −> real − min (5.8, ˜ 0.5); > val it = ˜ 0.5 : real

Page 16: Lectures FP

Въведение в стандарта ML

Функционално програмиране

16

Когато функцията min се прилага над двойката (5.8,˜ 0.5), двойката се съпоставя срещу образеца (x,y) във функционалната дефиниция. Двойката и образеца имат еднаква форма и затова съпоставянето е успешно. Тогава променливата x получава като стойност (привързва се към) реалното число 5.8, а y към ˜ 0.5.

При val декларациите също може да се съпоставя стойност срещу образец: всяка променлива в образеца се отнася към съответната компонента. Нека да разгледаме функцията scalevec, която връща наредена двойка от реални числа. Тази двойка може да бъде именована като се използва val декларация.

− val c = scalevec (1.0, (2.5, ˜2.2)); > val c = (2.5, ˜2.2) : real * real

Компонентите на наредената двойка също могат да получат имена, например xs и ys, като съпоставим образеца (xs, ys) срещу резултата от прилагането на функцията scalevec над конкретни стойности.

− val (xs, ys) = scalevec (2.0, it); > val xs = 5.0 : real > val ys = ˜ 4.4 : real

Функцията #i е стандартна функция в ML и чрез нея може да се избира i-тата компонента на наредена n-торка.

− #2 (7, 5); > val it = 5 : int

3. Тип unit Типът unit се използва с процедурното програмиране в ML. Процедура е

функция, чийто резултат е от тип unit. Тя се извиква заради ефекта от нейното изпълнение, а не заради нейната стойност, която винаги е (), т.е. n-торка без компоненти.

− (); > val it = () : unit

Повечето ML системи снабдяват с функция use с тип string → unit. Тази функция позволява да бъдат прочитани в ML дефиниции, които са предварително записани във файл.

− use "file_name";

Page 17: Lectures FP

Въведение в стандарта ML

Функционално програмиране

17

4. Полиморфни функции Ако при определянето на типа някои типове са оставени напълно

неограничени тогава дефиницията е полиморфна, т.е. имаща много форми. − fun pairself x = (x, x); > val it = fn : ′a −> ′a * ′a

Типът на горната функция е полиморфен тъй като, той съдържа типови променливи, а именно ′a. Полиморфният тип е типова схема. Когато типовите променливи се заместят с конкретни типове се формира пример на схемата. Така, ако функцията pairself е приложена към реално число тогава тя има тип real → real × real.

− pairself 3.0; > val it = (3.0, 3.0) : real * real Да разгледаме функцията fst, която връща първата компонента на

наредена двойка. − fun fst (x, y) = x; > val it = fn : ′a * ′b −> ′a

Очевидно, горната функция е полиморфна с две типови променливи. Нейният аргумент е наредена двойка въвличаща всеки два типа, а резултата и е от първият тип.

− fst (5, 7); > val it = 5: int − fst ((5,7), 9.0); > val it = (5, 7) : int * int В дефиницията на функцията fst и двата аргумента имат имена, но

единствено името на първия аргумент се използва при описването на тялото на функцията. В ML може да се използва подчертаващо тире “_” (“wild card”) за да се покаже, че част от аргумента е ненужен.

− fun fst (x, _ ) = x; > val it = fn : ′a * ′b −> ′a

5. Записи Записа е n-торка чиито компоненти се наричат полета и имат етикети.

Докато компонентите на n-торка се идентифицират чрез своята позиция от 1 до n, полетата на записа могат да се появяват в произволен ред. Така например,

Page 18: Lectures FP

Въведение в стандарта ML

Функционално програмиране

18

записите {x=5, y=true} и {y=true, x=5} са еднакви, тъй като записа се разглежда като множество от компоненти, а не като наредена последователност.

− {x=5, y=true}; > val it = {x=5, y=true} : {x : int, y : bool} − {x=5, y=true} = {y=true, x=5}; > val it = true : bool Както за наредените n-торки ML системата автоматично снабдява с

функции селектори за всички компоненти на записа. Така, ако искаме от горния запис да получим стойността на полето с етикет y ние пишем #y.

− val person = {name = "John", weight = 86, height = 186}; > val person = {height = 186, name = "John", weight = 86} : {height : int, name : string, weight : int} − #name person > val it = "John" : string За да се изберат стойностите на полетата от записа се използва също

съпоставяне на образци. Възможен образец за променливата person е {name = n, weight = w, height = h}, където на променливите n, w и h ще бъдат дадени стойностите на полетата name, weight и height, съответно.

− val {name = n, weight = w, height = h} = person; > val n = "John" : string > val w = 86 : int > val h = 186 : int

Ако не се нуждаем от стойностите на всички полета на мястото на останалите ние можем да поставим три точки (...), т.е. “record wild card”.

− val {name = n, height = h, . . .} = person; > val n = "John" : string > val h = 186 : int Да разгледаме сега функцията over_weighted, която проверява дали човек

е с наднормено тегло, т.е. проверява дали теглото му увеличено с 100 превишава неговата височина.

− fun over_weighted {name, weight, height} = weight + 100 > height; > val over_weighted = fn : {height : int, name : ′a, weight : int} −> bool − over_weighted person; > val it = false : bool

Page 19: Lectures FP

Въведение в стандарта ML

Функционално програмиране

19

Аргументът на функцията е запис с три полета. Тъй като полето с етикет name не участва в дясната част на дефиницията неговият тип не може да бъде определен. Този проблем може да се избегне чрез изрично задаване на типа на това поле.

− fun over_weighted {name : string, weight, height} = weight + 100 > height;

> val over_weighted = fn : {height : int, name : string, weight : int} −> bool

Page 20: Lectures FP

Въведение в стандарта ML

Функционално програмиране

20

Задачи за упражнение

1. Да се дефинира функция, която при зададено цяло число връща като

резултат самото число и информация дали то се дели на 13. 2. Да се дефинира функция divide, която за две дадени цели числа проверява

дали първото дели второто. 3. Да се дефинира функция average, която изчислява средно аритметично на

две дадени реални числа. 4. Да се дефинира функция max намираща по-голямото от две реални

числа.

5. Дадени са целите числа i и j. Да се дефинира функция div_mod, която

намира частното и остатъка при деленето на i на j.

6. Да се дефинира функция power изчисляваща ab , където a и b са дадени

реални числа.

7. Да се дефинира функция, която проверява дали време от деня

представено във формата (часове, минути, AM/PM) предхожда друго.

Така например, (10, 34, "AM") е преди (3, 52, "PM").

8. Да се дефинира функция, която изчислява лицето на триъглник по дадени

три страни a, b и c.

9. Да се дефинира функция, която намира каква сума ще имате във вашата

банкова сметка след даден брой години при зададена начална сума и

лихва.

10. Да се дефинира функция, която намира разликата на два вектора.

11. Да се дефинира функция, която намира разстоянието между два вектора.

12. Човек е представен чрез запис с две полета: име и височина. Да се

дефинира функция, която дава името на по-високия от дадени двама.

Page 21: Lectures FP

Въведение в стандарта ML

Функционално програмиране

21

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 3

РЕКУРСИВНИ ДЕФИНИЦИИ НА ФУНКЦИИ. ЛОКАЛНИ

ДЕКЛАРАЦИИ

1. Рекурсивни дефиниции на функции Рекурсията е фундаментална във функционалното програмиране.

Пресмятането на факториела на естествено число е типичен пример на рекурсия: 0! = 1 и n! = n × (n − 1)! ако n > 0. Тази задача в ML се решава чрез рекурсивна функция, т.е. функция в чиято дефиниция има обръщение към самата нея.

− fun fact n = if n=0 then 1 else n * fact (n - 1); > val fact = fn : int −> int − fact 3; > val it = 6 : int

Нека да видим как ML изчислява fact(3). Правилото за изчисление в ML е известно като заместване по стойност, защото функцията винаги дава стойности на своите аргументи. Така, първо 3 замества n в тялото на функцията.

if 3 = 0 then 1 else 3 * fact (3 - 1) Тъй като 3 = 0 е лъжа то условният израз се редуцира до 3 × fact (3 - 1). После (3 – 1) се изчислява и изразът се трансформира в 3 × fact(2). Това продължава докато условният израз върне true (виж Фигура 3.1). Очевидно, изчисляването на fact(3) следва математическата му дефиниция.

От Фигура 3.1 се вижда, че с прогресирането на рекурсията броя на числата които чакат да бъдат умножени нараства. Умножението може да бъде извършено едва след като рекурсията приключи. Така, за fact(3) изразът който трябва да бъде изчислен е 3 × (2 × (1 × 1)). Следователно, функцията fact не е ефективна по отношение на заеманата памет. Нека да дефинираме функция, която да извършва умножението веднага, например:

3 × (2 × fact (1)) = (3 × 2) × fact (1)= 6 × fact (1). − fun facti (n, p) = if n=0 then p else facti (n – 1, n * p); > val facti = fn : int * int −> int

Page 22: Lectures FP

Въведение в стандарта ML

Функционално програмиране

22

Функцията facti има допълнителен аргумент p, в който се пази резултата от умножението. Началната стойност на p трябва да бъде 1.

− facti (3, 1); > val it = 6 : int

Фигура 3.1: Изчисляване на fact(3) fact(3) => 3 × fact (3 - 1)

=> 3 × fact (2) => 3 × (2 × fact (2 - 1)) => 3 × (2 × fact (1))

=> 3 × (2 × (1× fact (1 - 1))) => 3 × (2 × (1× fact (0))) => 3 × (2 × (1× 1))

=> 3 × (2 × 1) => 3 × 2

=> 6 Фигура 3.2: Изчисляване на facti(3, 1)

facti(3, 1) => facti (3 – 1, 3 × 1) => facti (2, 3) => facti (2 – 1, 2 × 3) => facti (1, 6)

=> facti (1 – 1, 1 × 6) => facti (0, 6) => 6

При изчисляването на facti(3, 1) (виж Фигура 3.2) всяко умножение се извършва веднага и по този начин междинният израз остава малък и изискванията към паметта са константа. Изчислението е итеративно или също може да бъде използван термина крайно рекурсивно, тъй като резултата от рекурсивното извикване facti(n - 1, n × p) не подлежи на по-нататъшни изчисления, а веднага се връща като стойност на facti(n, p). Рекурсивното извикване в fact не е

Page 23: Lectures FP

Въведение в стандарта ML

Функционално програмиране

23

крайно, защото неговата стойност претърпява по-нататъшна обработка, а именно умножение с n.

Много функции могат да бъдат направени итеративни чрез добавяне на допълнителен аргумент, както p в facti. С някои компилатори итеративните функции могат да бъдат по-бързи с други по-бавни. Понякога, правейки функцията итеративна е единственият начин да се избегне недостига на памет. Когато нямаме такива причини, рекурсивните функции би трябвало да се изразяват в тяхната естествена форма. Въпреки, че facti е по-ефективна от fact, тя е по-малко ясна.

2. Изчисляване на условните изрази Нека да разгледаме как се пресмята условния израз if e then e1 else e2. ML

изчислява e1 единствено ако e има стойност true, и e2 единствено ако e е false, т.е. пресмятането се извършва само ако е необходимо (наричано още lazy evaluation). По-нататък, изразите e1 andalso e2 и e1 orelse e2, в които участват булевите оператори andalso и orelse, заместват логическите изрази if e1 then e2 else false и if e1 then true else e2, съответно. Очевидно, те се изчисляват последователно, т.е. първо се пресмята стойността на e1, а e2 се изчислява само ако е необходимо. Това последователно пресмятане прави булевите изрази andalso и orelse подходящи за дефиниране на рекурсивни предикати (булеви функции). Да разгледаме, като пример, функция която проверява дали е едно число е степен на две.

− fun even n = (n mod 2 = 0); > val even = fn : int −> bool − fun powoftwo n = (n=1) orelse (even (n) andalso powoftwo (n div 2)); > val powoftwo = fn : int −> bool

От Фигура 3.3, представяща изчисляването на стойността на функцията powoftwo за аргумент 6, се вижда, че изчисленията прекъсват в момента в който изхода е решен.

− powoftwo 6; > val it = false

Следователно, условните изрази и булевите оператори в ML не се изчисляват като функциите. Иначе, правилото заместване по стойност (strict evaluation) би пресмятало винаги и двата аргумента. Всички други инфиксни оператори в ML са действителни функции.

Page 24: Lectures FP

Въведение в стандарта ML

Функционално програмиране

24

Фигура 3.3: Изчисляване на powoftwo(6)

powoftwo(6) => (6=1) orelse (even(6) andalso …) => even(6) andalso powoftwo (6 div 2) => powoftwo (3)

=> (3=1) orelse (even(3) andalso …) => even(3) andalso powoftwo (3 div 2) => false

3. Пример за съставяне на рекурсивна функция Тъй като рекурсията е фундаментална за функционалното програмиране

ще разгледаме пример за съставяне на рекурсивна функция. Рекурсията се заключава в редуциране на разглеждания проблем до по-малки подпроблеми. Ключът към ефективността е избиране на правилните подпроблеми.

ML няма оператор за повдигане на степен. Очевиден начин за изчисляване на xk е чрез повторно умножение на x. Всъщност, като използваме рекурсията задачата xk може да се редуцира до подзадачата xk-1. Обаче, когато имаме четна степен, например x10 можем да изчислим x5 и после да го повдигнем на квадрат, т.е. не е необходимо да извършваме 10 умножения. Предвид на това, че x5 = x × x4, ние можем да изчислим x4 чрез повдигане на квадрат също:

x10 = (x5)2 = (x × x4)2 = (x × (x2)2)2 Така, използвайки закона x2n = (xn)2 решението е подобрено значително сравнение с повторните умножения. Следващата стъпка е да избегнем вложените повдигания на квадрат като заменим (xn)2 с (x2)n :

210 = 45 = 4 × 162 = 4 × 256 = 1024 Нека сега да дефинираме функция power изчисляваща xk за x реално

число и k положително цяло число. − fun power (x, k) : real = if k = 1 then x else if k mod 2 = 0 then power (x * x, k div 2) else x * power (x * x, k div 2);

> val power = fn : real * int −> real

Page 25: Lectures FP

Въведение в стандарта ML

Функционално програмиране

25

− power (2.0, 10); > val it = 1024.0 : real

Очевидно, функцията power е итеративна в първото си рекурсивно извикване благодарение на редуцирането на x2n до (x2)n вместо до (xn)2. Второто извикване, в случая на нечетен степенен показател, може да бъде направено итеративно чрез добавяне на аргумент който да пази резултата, което е излишно усложнение.

4. Локални декларации

В някои програми са необходими помощни декларации, които са валидни единствено локално. В ML има два типа локални декларации: декларации локални за изрази и декларации локални за други декларации. Нека да илюстрираме и двата типа с примери.

Локални декларации в изрази Опростяването на дроб n / d, т.е. редуцирането до положение когато n и d

нямат общ делител, включва разделянето на двете числа на най-голямия им общ делител. Да дефинираме първо функцията gcd, която намира най-големия общ делител на две цели числа по алгоритъма на Евклид.

− fun gcd (m, n) = if m = 0 then n else gcd (n mod m, m); > val gcd = fn : int * int −> int

Тогава функцията решаваща горната задача ще изглежда както следва: − fun fraction (n, d) = (n div gcd (n, d), d div gcd (n, d));

> val fraction = fn : int * int −> int * int Както се вижда, функцията fraction изчислява два пъти най-големия общ делител на n и d. За да избегнем това резултата от gcd може да бъде запазен в локална променлива.

− fun fraction (n, d) = let val com = gcd (n, d) in (n div com, d div com) end;

Така ние използвахме let декларация, която има вида: let D in E end; Декларацията D е валидна единствено локално в израза E. Първо се изчисляват изразите в D и на резултатите се дават имена, а после се изчислява израза E. Обикновенно D е последователност от декларации и ефекта от всяка декларация е видим в следващата. Като илюстрация на let декларациите нека да разгледаме функция, която намира корен квадратен от a при зададено начално приближение x и точност eps. Следващото приближение се получава по формулата: (a/x + x)/2.

Page 26: Lectures FP

Въведение в стандарта ML

Функционално програмиране

26

− fun findroot (a, x, eps) = let val nextx = (a/x +x) / 2.0 in if abs (x − nextx) < eps * x then nextx else findroot (a, nextx, eps) end;

> val findroot = fn : (real * real * real) −> real Функцията sqroot извиква findroot с подходящи начални стойности.

− fun sqroot a = findroot (a, 1.0, 1.0E˜10); > val sqroot = fn : real −> real

Забелязваме, че аргументите a и eps се подават непроменени при всяко рекурсивно извикване на функцията findroot. Те могат да бъдат направени глобални за findroot чрез let декларация влагаща findroot в sqroot.

− fun sqroot a = let val eps = 1.0E˜10 fun findroot x =

let val nextx = (a/x +x) / 2.0 in if abs (x − nextx) < eps * x then nextx else findroot nextx end

in findroot 1.0 end; > val sqroot = fn : real −> real − sqroot 4.0; > val it = 2.0 : real

Локални декларации в декларации Локална декларация приличаща на let декларацията е: local D1 in D2 end;

Декларацията D1 е видима единствено в декларацията D2. D1 и D2 могат да декларират произволен брой имена. Отбелязваме, че let се използва много по-често от local. Основната роля на local декларацията е да скрива декларации. Така в следващата дефиниция функцията divide може да бъде извикана само от функцията anniversary.

− local fun divide (x, y) = y mod x = 0 in fun anniversary age = divide (10, age) orelse divide (25, age) end; > val anniversary = fn : int −> bool

Page 27: Lectures FP

Въведение в стандарта ML

Функционално програмиране

27

Едновременни декларации С едновременните декларации могат да се дефинират няколко имена

наведнъж. Val декларация от вида val Id1 = E1 and … and Idn = En;

изчислява първо изразите E1, ..., En и след това идентификаторите Id1, ..., Idn получават съответните стойности. Тъй като, декларациите са валидни едва след като всички изрази са изчислени то редът им не е от значение. − val x = 3 and y = 15 and z = 21;

> val x = 3 : int > val y = 15 : int > val z = 21 : int

С едновременните декларации могат също да се разменят стойностите на променливите.

− val z = y div x and x = z; > val z = 5 : int > val x = 21 : int

С едновременните декларации могат да се дефинират взаимно рекурсивни функции. Няколко функции са взаимно рекурсивни ако те се описват рекурсивно едни други. Като пример нека да разгледаме следващата сума.

π/4 = 1− 1/3 + 1/5 − 1/7 + ...+ 1/(4к + 1) − 1/(4к + 3) + ... Чрез взаимната рекурсия последния елемент от сумирането може да бъде или положителен или отрицателен.

− fun pos d = neg (d – 2.0) + 1.0/d and neg d = if d > 0.0 then pos (d – 2.0) – 1.0/d else 0.0; > val pos = fn : real −> real > val neg = fn : real −> real

С горната дефиниция са зададени две функции. − 4.0 * pos (201.0); > val it = 3.151493401

Тези две функции могат да бъдат комбинирани в една функция чрез използването на допълнителен аргумент.

− fun sum (d, one) = if d > 0.0 then sum (d – 2.0, ˜one) + one/d else 0.0;

Page 28: Lectures FP

Въведение в стандарта ML

Функционално програмиране

28

Задачи за упражнение

1. Да се дефинира функция, която намира броя на цифрите в десетичния запис на естественото число n.

2. Да се дефинира функция, която установява дали в записа на естественото число n се съдържа цифрата k.

3. Дадено е естественото число n. Да се дефинира функция, в резултат от изпълнението на която се получава число записано със същите цифри, но в обратен ред.

4. Да се дефинира функция, която превръща десетичното число z в число записано в осмична бройна система.

5. Да се дефинира функция, която проверява дали естественото число n е просто.

6. Естественото число n е просто ако не се дели не никое от простите числа в интервала [2, n/2]. Да се дефинира функция, която проверява дали n е просто число като използва горното.

7. Да се дефинира функция, която проверява дали естественото число n четено отляво надясно и отдясно наляво е еднакво.

8. Да се напише рекурсивна функция, която намира стойността на функцията на Акерман Ack (m, n), дефинирана за m ≥ 0 и n ≥ 0 както следва: Ack (0, n) = n + 1 Ack (m, 0) = Ack (m – 1, 1), m > 0 Ack (m, n) = Ack (m – 1, Ack (m, n – 1)), m > 0, n > 0.

9. Дa се дефинира функция, която изчислява приближено сумата: ex = 1 + x/1! + x2/2! + … Пресмятането да продължи, докато абсолютната стойност на последното добавено събираемо стане по-малка от ε (x и ε>0 са дадени реални числа).

10. Дa се дефинира функция, която пресмята приближено стойността на следната безкрайна сума S = x/1! − x3/3! + x5/5! − x7/7! + … за произволно x от интервал [0, 1]. Събирането да продължи докато се добави събираемо по абсолютна стойност по-малко от ε (ε е дадено достатъчно малко положително число).

Page 29: Lectures FP

Въведение в стандарта ML

Функционално програмиране

29

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 4

СПИСЪЦИ. ФУНКЦИИ НАД СПИСЪЦИ.

1. Списъци Списъкът е крайна последователност от еднотипни елементи.

− [4, 3, ˜2, 3]; > val it = [4, 3, ˜2, 3] : int list

Редът на елементите в списъка е от значение и елементите могат да се появяват повече от веднъж. Списъкът може също да бъде празен [], т.е. да не съдържа елементи. Празният списък има полиморфен тип ′a list.

− []; > val it = [] : ′a list

Елементите на списъка могат да имат произволен тип включително наредени n-торки и даже други списъци.

− [[4, 3], [], [2]]; > val it = [[4, 3], [], [2]] : int list list

Построяване на списък Всеки списък е построен чрез два примитива: константа nil и инфиксния

оператор ::. Константата nil е синоним на празен списък, т.е. [], а операторът :: създава списък чрез поставяне на елемент в началото на съществуващ списък.

− 9 :: nil; > val it = [9] : int list − 5 :: it; > val it = [5, 9] : int list

Всеки списък е или празен или има формата x :: l, където x е глава на списъка, а l опашка. Опашката на списъка е също списък. Отбелязваме, че операциите върху списъци не са симетрични, т.е. първият елемент е много по-лесно достъпен от останалите. Нека да видим как ще изглежда функцията, която построява списък от целите числа от m до n, т.е. [m, m+1, …, n]. Първо сравняваме m и n и ако m > n

Page 30: Lectures FP

Въведение в стандарта ML

Функционално програмиране

30

списъкът е празен. Иначе, главата на списъка е m, а опашката [m+1, …, n]. Конструираме опашката рекурсивно и получаваме резултата: m :: [m+1, …, n].

− fun upto (m, n) = if m > n then [] else m :: upto (m+1, n); > val upto = fn : int * int −> int list − upto (3, 7); > val it = [3, 4, 5, 6, 7] : int list

Съпоставяне на образци и операции над списъци Както вече споменахме, основният начин за извличане на компонентите

на съставните типове данни е чрез съпоставяне на образци. Тъй като, списъците могат да бъдат конструирани по два начина ни са нужни два алтернативни образеца. За празният списък се използва константата образец nil, докато за списъка построен чрез оператора ::, се използва конструктор заграден с кръгли скоби. Нека като пример да дефинираме функция, която намира произведението на списък от цели числа. Операциите върху списъци обикновенно се дефинират рекурсивно чрез разглеждането на няколко случая.

− fun prod nil = 1 | prod (x :: xs) = x * (prod xs); > val prod = fn : int list −> int

Функцията prod се състои от две клаузи разделени с вертикална черта ( | ). Всяка клауза разглежда един аргумент образец. Възможно е, да има повече от две клаузи и по-сложни образци.

− prod [2, 4, 5]; > val it = 40 : int

Най-често анализираните случаи са празен срещу непразен списък. Обаче, намирането на най-големия елемент на списък от цели числа изисква нещо различно. Така, ние разглеждаме следните два случая: едноелементен списък [x] и списък съдържащ повече от два елемента [x1, x2, …].

− fun maxl [x] : int = x | maxl (x1 :: x2 :: xs) = if x1 > x2 then maxl (x1 :: xs) else maxl (x2 :: xs); ! Warning : pattern matching is not exhaustive > val maxl = fn : int list −> int

Нека да обърнем внимание на предупреждаващото съобщение: pattern matching is not exhaustive. ML открива, че функцията maxl не е дефинирана за празен

Page 31: Lectures FP

Въведение в стандарта ML

Функционално програмиране

31

списък и отпечатва предупреждение. Следователно тя работи за всички стойности, освен за празен списък.

− maxl [˜4, 3, 0, ˜2, 3]; > val it = 3 : int − maxl []

! Uncaught exception: ! Match

Така, при последното извикване на функцията maxl тя е била приложена над аргумент за който не е дефинирана и това прекъсва изпълнението.

Символни низове и списъци В стандарта ML string е примитивен тип и символ е просто низ с дължина

едно. Стандартната функция explode превръща символен низ в списък от символи. Докато стандартната функция implode извършва обратната операция, т.е. свързва списък от символи в низ.

− explode "rain"; > val it = ["r", "a", "i", "n"] : string list − implode it; > val it = "rain" : string

2. Фундаментални функции над списъци

За даден списък ние можем да намерим броя на елементите му, да изберем n елемента, да вземем префикс или суфикс, да обърнем реда на елементите и т.н. Функциите дефинирани в тази секция са необходими и повечето ML системи са снабдени с тях. Например, функциите за добавяне на списък към друг списък (@) и за обръщане на списък (rev) често са част от езика.

Функции: null, hd и tl Трите основни функции над списъци са null, hd и tl. Функцията null

проверява дали списък е празен. − fun null [] = true | null (_ :: _) = false; > val null = fn : ′a list −> bool

Page 32: Lectures FP

Въведение в стандарта ML

Функционално програмиране

32

Функцията е полиморфна, защото за да провери дали списъка е празен тя не тества неговите елементи. За това във втория образец се използват подчертаващи тирета (wild cards) на мястото на компонентите.

− null []; > val it = true : bool − null [[]]; > val it = false : bool

Отбелязваме, че [[]] не е празен списък, тъй като той съдържа един елемент, а именно празен списък.

Функцията hd връща първия елемент (главата) на непразен списъка. − fun hd (x :: _) = x; ! Warning : pattern matching is not exhaustive > val hd = fn : ′a list −> ′a

Тук образеца има подчертаващо тире на мястото на опашката. Тъй като, функцията hd не е определена за празен списък, ML отпечатва предупреждение. Тя е частична функция като maxl.

− hd [[1, 2, 3], [4, 5], [6]]; > val it = [1, 2, 3] : int list − hd it; > val it = 1 : int Функцията tl връща опашката на непразен списъка, т.е. всички елементи

на списъка без първия. − fun tl (_ :: xs) = xs; ! Warning : pattern matching is not exhaustive > val tl = fn : ′a list −> ′a list

Както hd, тази функция е частична. Резултата от нея винаги е списък. − tl [[1, 2, 3], [4, 5], [6]]; > val it = [[4, 5], [6]] : int list list − tl it; > val it = [[6]] : int list list

Чрез използването на функциите null, hd и tl всички други ML функции могат да бъдат записани без съпоставяне на образци. Например, функцията за произведението на списък от цели числа ще изглежда като:

Page 33: Lectures FP

Въведение в стандарта ML

Функционално програмиране

33

− fun prod l = if null l then 1 else (hd l) * prod (tl l); > val prod = fn : int list −> int

Отбелязваме, че съпоставянето на образци е за предпочитане, тъй като функцията описана по този начин е по-ясна. Освен това, повечето ML компилатори анализират множеството от образци за да генерират най-ефективния код за функцията. Най-важното е, че компилатора отпечатва предупреждение ако образциите не покриват всички възможни аргументи за функцията.

Функции: length, take и drop Сега ще разгледаме функциите length, take и drop, които извършват

следните операции: l = [x1, …, xi, xi+1, …, xn] length (l) = n

take (i, l) = [x1, …, xi] drop (i, l) = [xi+1, …, xn] Дължината на списък (броя на елементите) може да бъде изчислена чрез рекурсия.

− fun len [] = 0 | len (_ :: xs) = 1 + len xs; > val len = fn : ′a list −> int − fun len [[1, 2], [3, 4, 5]] > val it = 2 : int

На Фигура 4.1 е показано как len изчислява дължината на списък съдържащ целите числа от 1 до 10 000. Фигура 4.1: Изчисляване на len [1, 2, …, 10 000]

len [1, 2, …, 10 000] => 1 + len [2, …, 10 000] => 1 + (1 + len [3, …, 10 000]) . . .

=> 1 + (1 + 9998) => 1 + 9999 =>10 000

Page 34: Lectures FP

Въведение в стандарта ML

Функционално програмиране

34

Очевидно, функцията не е ефективна за дълги списъци, тъй като сумирането на единиците започва едва след като рекурсията приключи. Итеративната версия, използваща допълнителен аргумент за натрупването на единиците, е по-добра, защото сумирането се извършва веднага.

− fun length l = let fun len (n, []) = n | len (n, _ :: xs) = len (1+n, xs) in len (0, l) end; > val length = fn : ′a list −> int

Извикването на функцията take(i, l) връща списък съдържащ първите i елемента на списъка l.

− fun take (i, []) = [] | take (i, x :: xs) = if i > 0 then x :: take (i-1, xs) else []; > val take = fn : ′a list −> ′a list − take (3, [1, 3, 5, 7]); > val it = [1, 3, 5] : int list

От Фигура 4.2 забелязваме, че 1 :: (3 :: (5 :: [])) е израз. Изчисляването му до списъка [1, 3, 5] консумира време. Освен това заеманата памет зависи от дължината на списъка. Фигура 4.2: Изчисляване на take (3, [1, 3, 5, 7])

take (3, [1, 3, 5, 7]) => 1 :: take (2, [3, 5, 7]) => 1 :: (3 :: take (1, [5, 7])) => 1 :: (3 :: (5 :: take (0, [7])))

=> 1 :: (3 :: (5 :: [])) => 1 :: (3 :: [5]) => 1 :: [3, 5] => [1, 3, 5] Нека да разгледаме итеративния вариант на take. Функцията rtake, дадена

по-долу, е по-ефективна тъй като, рекурсията е плитка. Обаче, както може да се забележи редът на елементите в списъка е обърнат. Очевидно, тази функция може да бъде използвана когато редът на елементите не е от значение.

Page 35: Lectures FP

Въведение в стандарта ML

Функционално програмиране

35

− fun rtake (_ , [], ls) = ls | rtake ( i, x :: xs, ls) = if i > 0 then rtake (i-1, xs, x :: ls) else ls; > val rtake = fn : ′a list −> ′a list − rtake (3, [1, 3, 5, 7], []); > val it = [5, 3, 1] : int list Резултата от drop(i, l) е списък съдържащ всичко без първите i елемента

на l. − fun drop (_ , []) = [] | drop (i, x :: xs) = if i > 0 then drop (i-1, xs) else (x :: xs); > val drop = fn : ′a list −> ′a list − drop (3, [1, 3, 5, 7]); > val it = [7] : int list

За щастие рекурсията в функцията drop е итеративна (крайна).

Функции: @ и rev Нека да разгледаме инфиксния оператор @, който добавя един списък

към друг и функцията rev, която обръща реда на елементите в списъка. ML е снабден и с двете функции. Тъй като ефективността на много функции над списъци зависи от тях тук ще им обърнем по-специално внимание.

Дефиницията на функцията @ е следната: − infix @; − fun [] @ ys = ys | (x::xs) @ ys = x :: (xs @ ys); > val @ = fn : ′a list * ′a list −> ′a list − [1, 3, 5] @ [7, 9]; > val it = [1, 3, 5, 7, 9] : int list

Очевидно, резултатният списък се получава като елементите на първия списък се добавят към втория, т.е. първият списък се построява отново (виж Фиг. 4.3). Така стойността на изчисляването на xs @ ys е пропорционалнна на дължината на xs. Дори да имаме израза xs @ [] пак се прави копие на xs. Функцията, която обръща реда на елементите в списъка може да бъде дефинирана като се използва @. Просто главата на списъка, представена като едноелементен списък, се добавя към обърнатата опашка.

Page 36: Lectures FP

Въведение в стандарта ML

Функционално програмиране

36

− fun rrev [] = [] | rrev (x :: xs) = rrev (xs) @ [x]; > val rrev = fn : ′a list −> ′a list

Горната функция, обаче не е ефективна, тъй като ако имаме списък с дължина n > 0 то @ извиква n-1 пъти оператора :: за да копира обърнатата опашка. Конструирането на списъка [x] извиква :: отново, т.е. общо n извиквания. Освен това обръщането на опашката извиква още n-1 пъти оператора :: и т.н. Следователно общият брой извиквания на оператора :: е 0 + 1 + 2 + ... + n = n(n+1)/2 (виж Фигура 4.3). Фигура 4.3: Изчисляване на [1, 3, 5] @ [7, 9]

[1, 3, 5] @ [7, 9] => 1 :: ([3, 5] @ [7, 9]) => 1 :: (3 :: ([5] @ [7, 9])) => 1 :: (3 :: (5 :: ([] @ [7, 9]))) => 1 :: (3 :: (5 :: [7, 9])) => 1 :: (3 :: [5, 7, 9]) => 1 :: [3, 5, 7, 9] => [1, 3, 5, 7, 9]

В rtake беше показан друг начин за обръщане на списък, т.е. чрез

повторно преместване на елементите от един списък в друг. − fun rev l = let fun rrev ([], ys) = ys | rrev (x :: xs, ys) = rrev (xs, x :: ys) in rrev (l, []) end; > val rev = fn : ′a list −> ′a list − rev [1, 3, 5, 7, 9]; > val it = [9, 7, 5, 3, 1]

В горната дефиниция @ не се извиква и броят на стъпките е пропорционален на дължината на списъка l. Тази ключова идея, а именно натрупването на елементите на списък в допълнителен аргумент вместо използването на оператора за добавяне (@), се прилага при съставянето на много други функции.

Page 37: Lectures FP

Въведение в стандарта ML

Функционално програмиране

37

Задачи за упражнение

1. Да се дефинира функция, която намира средно аритметично на елементите на списък от цели числа.

2. Да се дефинира функция, която връща последния елемент на даден списък.

3. Да се дефинира функция, която премахва последния елемент от даден списък.

4. Да се дефинира функция, която преброява колко пъти даден елемент се среща в даден списък.

5. Да се дефинира функция, която преобразува даден списък така, че в него да не се съдържат повтарящи се елементи.

6. Да се дефинира функция takeij, която за даден списък l връща списъка съдържащ елементите от i до j на l, където i и j са дадени естествени числа. Например, takeij (2, 4, [1, 2, 3, 4, 5, 6]) = [2, 3, 4].

7. Да се дефинира функция flat, която за даден списък от списъци връща списък съдържащ елементите на отделните списъци, например flat [[1], [2, 3]] = [1, 2, 3].

8. Да се дефинира функция combine, която за два дадени списъка връща списък от наредените двойки на съответните елементи в списъците. Например, combine ([1, 3, 5], [2, 4, 6]) = [(1, 2), (3, 4), (5, 6)].

9. Да се дефинира функция split (обратна на combine), която за даден списък от наредени двойки връща наредена двойка от списъците съдържащи съответните елементи на разделените двойки. Например, split ([(1, 2), (3, 4), (5, 6)]) = ([1, 3, 5], [2, 4, 6]).

10. Да се дефинира функция rem, която от даден символен низ премахва всички появявания на даден символ.

11. Да се дефинира функция revstring, която обръща символен низ. 12. Да се дефинира функция, която проверява дали даден символен низ е

палиндром, т.е. еднакъв при четене отляво надясно и отдясно наляво. 13. Да се дефинира функция to_dec, която за даден символен низ съдържащ

бинарно число връща съответното десетично. Например to_dec "110" = 6. 14. Да се дефинира функция position, която връща позицията на подниз в

друг низ. Например position ("end", "extend") = 4.

Page 38: Lectures FP

Въведение в стандарта ML

Функционално програмиране

38

Page 39: Lectures FP

Въведение в стандарта ML

Функционално програмиране

39

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 5

ПРИЛОЖЕНИЕ НА СПИСЪЦИ: БИНАРНА АРИТМЕТИКА,

МАТРИЦИ, МНОЖЕСТВА

Тук ще демонстрираме как списъците могат да бъдат използвани за

решаване на по-сложни задачи, като бинарна аритметика и операции над

множества и матрици.

1. Бинарна аритметика Нека да дефинираме бинарно събиране и умножение за списъци от нули и

единици. Така, бинарният запис на числото 30 ще бъде представен чрез списъка

[1, 1, 1, 1, 0].

Събиране Събирането на две бинарни числа се извършва отдясно наляво. Двата

бита плюс преноса отдясно дават стойността на съответния бит на сумата и преноса наляво. Нека да припомним като съберем бинарните записи на числата 30 и 11.

11110 + 1011 101001 Припомняме, че най-левият елемент на списъка е най-лесно достъпен. Следователно, сумирането отдясно наляво не е подходящо когато числата са представени чрез списъци. За това, числата ще се пазят в обратен ред и събирането ще се извършва отляво надясно. Двете бинарни числа могат да имат различни дължини. Когато единият битов лист свърши преноса трябва да се разпространи по битовете на другия. Такова е предназначението на следващата функция.

− fun bincarry (0, ps) = ps | bincarry (1, []) = [1] | bincarry (1, p :: ps) = (1 - p) :: bincarry (p, ps);

Page 40: Lectures FP

Въведение в стандарта ML

Функционално програмиране

40

! Warning : pattern matching is not exhaustive > val bincarry = fn : int * int list −> int list

Както се вижда от горната дефиниция, функцията bincarry е частична, тъй като тя разглежда единственно стойностите 0 и 1, т.е. двете възможни стойности на преноса. Функцията binsum, която извършва сумирането на двете бинарни числа, има три аргумента: двата списъка съдържащи числата в обратен ред и преноса.

− fun binsum (c, [], qs) = bincarry (c, qs) | binsum (c, ps, []) = bincarry (c, ps) | binsum (c, p :: ps, q :: qs) =

((c+p+q) mod 2) :: binsum ((c+p+q) div 2, ps, qs); > val binsum = fn : int * int list * int list −> int list

Нека да опитаме 30 + 11 = 41, като не забравяме, че списъците трябва да са в обратен ред.

− binsum (0, [0, 1, 1, 1, 1], [1, 1, 0, 1]); > val it = [1, 0, 0, 1, 0, 1] : int list

Умножение Умножението на бинарни числа се извършва чрез преместване и

събиране. Например, 30 × 11 = 330 : 11110

× 1011 11110 11110 + 11110

101001010 Преместването ще се осъществява чрез добавяне на 0 в началото на списъците, които ще бъдат събирани:

− fun binprod ([], _) = [] | binprod (0 :: ps, qs) = 0 :: binprod (ps, qs) | binprod (1 :: ps, qs) = binsum (0 , qs, 0 :: binprod (ps, qs)); ! Warning : pattern matching is not exhaustive > val binprod = fn : int list * int list −> int list

Page 41: Lectures FP

Въведение в стандарта ML

Функционално програмиране

41

− binprod ([1, 1, 0, 1], [0, 1, 1, 1, 1]); > val it = [0, 1, 0, 1, 0, 0, 1, 0, 1] : int list

2. Операции над матрици Матрицата може да се разглежда като списък от редове, а всеки ред като

списък от елементите на матрицата, т.е. списък от списъци. Например, − val A = [[1, 2, 3], [4, 5, 6]]; > val A = [[1, 2, 3], [4, 5, 6]] : int list list

Транспониране на матрица Функцията, която транспонира матрица заменя списъка от редове със

списък от колони, т.е. A = [[1, 2, 3], [4, 5, 6]] след транспонирането ще бъде

A = [[1, 4], [2, 5]

[3, 6]] Очевидно, за да се транспонира матрица трябва последователно да се вземат колоните от нея. Така, следващата функция извлича първата колона от матрица

− fun headcol [] = [] | headcol ((x :: _) :: rows) = x :: headcol rows; ! Warning : pattern matching is not exhaustive > val headcol = fn : ′a list list −> ′a list

а функцията tailcol връща матрицата от останалите колони, т.е. всички без първата.

− fun tailcol [] = [] | tailcol ((_ :: xs) :: rows) = xs :: tailcol rows; ! Warning : pattern matching is not exhaustive > val tailcol = fn : ′a list list −> ′a list list

Page 42: Lectures FP

Въведение в стандарта ML

Функционално програмиране

42

Нека да видим какъв е резултата след прилагането на двете функции върху матрицата A.

− headcol A; > val it = [1, 4] : int list − tailcol A; > val it = [[2, 3], [5, 6]] : int list list

Следващо, като използваме функциите headcol и tailcol ще дефинираме функцията transpose. Обръщаме внимание на факта, че рекурсията приключва когато имаме списък от празни списъци:

− fun transpose ([] :: rows) = [] | transpose rows = headcol rows :: transpose (tailcol rows); > val transpose = fn : ′a list list −> ′a list list − transpose А; > val it = [[1, 4], [2, 5], [3, 6]] : int list list

Умножение на матрици Нека първо да припомним как се умножават матрици. Ако A е m × k

матрица, а B е k × n матрица, то произведението им A × B е m × n матрица. Елемента с индекси (i, j) в матрицата A × B е резултат от умножението на i-я ред на A с j-тата колона на B, т.е.

(ai1, ai2, …, aik)(b1j, b2j, …, bkj) = ai1 b1j + ai2 b2j + …+ aik bkj . Да дефинираме функцията dotprod, реализираща горното умножение:

− fun dotprod ([], []) = 0.0 | dotprod (a :: as, b :: bs) = a * b :: dotprod (as, bs); ! Warning : pattern matching is not exhaustive > val dotprod = fn : real list * real list −> real

За да се получи i-я ред на матрицата A × B, i-я ред на A се умножава последователно с всички колони на B. Да предположим, че матрицата B е транспонирана. Следващата функция реализира умножението на матрица от един ред с матрицата B.

− fun rowprod (_ , []) = [] | rowprod (row, col :: cols) = dotprod (row, col) :: rowprod (row, cols); > val rowprod = fn : real list * real list list −> real list

Page 43: Lectures FP

Въведение в стандарта ML

Функционално програмиране

43

Очевидно, ако приложим горната функция за всички редове на A ще получим матрицата A × B.

− fun rowlistprod ([] , _) = [] | rowlistprod (row :: rows, cols) = rowprod (row, cols) :: rowlistprod (rows, cols); > val rowlistprod = fn : real list list * real list list −> real list list

Функцията matprod извиква rowlistprod с аргументи матрицата A и транспонираната на B.

− fun matprod (A , B) = rowlistprod (A, transpose B); > val matprod = fn : real list list * real list list −> real list list

Нека да декларираме две матрици и да тестваме matprod. − val A = [ [2.0, 0.0], [1.0, ˜1.0], [0.0, 1.0] ] and B = [ [ 1.0, 2.0], [˜1.0, 0.0] ]; − matprod (A, B); > val it = [[2.0, 4.0], [2.0, 2.0], [˜1.0, 0.0]] : real list list

3. Операции над множества Множествата също мога да бъдат представяни чрез списъци на ML. Да

дефинираме първо функция, която проверява дали елемент принадлежи на

дадено множество.

− infix mem;

− fun x mem [] = false

| x mem (y :: ys) = (x = y) orelse (x mem ys);

> val mem = fn : ′′a * ′′a list −> bool В отговора на ML системата в горната функция се съдържа типовата

променлива ′′a. Стандарта ML използва променливите ′′a, ′′b, … за да означи

типовете допускащи тест за равенство. Така, функцията mem е полиморфна, но

Page 44: Lectures FP

Въведение в стандарта ML

Функционално програмиране

44

в ограничен смисъл, тъй като тя може да се прилага за всеки списък, чийто

елементи могат да бъдат тествани за равенство.

Като използваме функцията mem можем да дефинираме функция, която

добавя елемент към списък само ако той не се съдържа вече в списъка.

− fun newmem (x, l) = if x mem l then l else x :: l;

> val newmem = fn : ′′a * ′′a list −> ′′a list Тъй като, множеството е съвкупност от неповтарящи се еднотипни елементи, то

функцията newmem построява списък, който може да бъде разглеждан като

крайно множество.

Обединение Обединението на две множества xs и ys е множество включващо всички

елементи на ys и онези на xs, които не са елементи на ys. Така елементите на множеството xs последователно се добавят към ys с функцията newmem.

− fun union ([], ys) = ys

| union (x :: xs, ys) = newmem (x, union (xs, ys));

> val union = fn : ′′a list * ′′a list −> ′′a list − union ([1, 2, 3], [0, 2, 4, 6]); > val it = [1, 3, 0, 2, 4, 6] : int list

Сечение Сечението на две множества xs и ys е множество включващо всички

елементи xs, които са елементи и на ys. − fun inter ([], ys) = []

| inter (x :: xs, ys) = if x mem ys then x :: inter (xs, ys)

else inter (xs, ys);

> val inter = fn : ′′a list * ′′a list −> ′′a list − inter ([1, 2, 3,], [0, 2, 4, 6]); > val it = [2] : int list

Page 45: Lectures FP

Въведение в стандарта ML

Функционално програмиране

45

Подмножество и равенство на множества Множеството xs е подмножество на ys ако всеки елемент на xs е също

елемент и на ys. − infix subset;

− fun [] subset ys = true

| (x :: xs) subset ys = (x mem ys) andalso (xs subset ys);

> val subset = fn : ′′a list * ′′a list −> bool − [2, 4, 6] subset [1, 2, 3, 4, 5, 6, 7];

> val it = true : bool Проверката за равенство на две множества е дефинирана в термините на функцията subset, т.е. следва математическата дефиниция за равенство на множества.

− infix seteq;

− fun xs seteq ys = (xs subset ys) andalso (ys subset xs);

> val seteq = fn : ′′a list * ′′a list −> bool − [2, 4, 6] seteq [2, 3, 4];

> val it = false : bool

Декартово произведение Декартово произведение на множества X и Y е множеството от всички

наредени двойки (x, y) такива, че x принадлежи на X и y на Y. Функцията cartprod реализира тази операция. В нейната дефиниция е използвана локалната функция xpair, която дава като резултат декартовото произведение на две множества от които едното е едноелементно.

− fun cartprod ([], ys) = []

| cartprod (x :: xs, ys) =

let fun xpair (_ , []) = []

| xpair (u, v :: vs) = (u, v) :: xpair (u, vs)

in xpair (x, ys) @ cartprod (xs, ys) end;

> val cartprod = fn : ′a list * ′b list −> (′a * ′b) list − cartprod ([1, 2, 3], [4, 5]);

> val it = [(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)] : (int * int) list

Page 46: Lectures FP

Въведение в стандарта ML

Функционално програмиране

46

Задачи за упражнение

1. Да се дефинира функция, която извършва деление на едно бинарно число

на друго. Числата са представени като списъци от 0 и 1. 2. Да се дефинира функция indexm, която избира елемент от дадени ред и

колона на матрица. 3. Да се дефинира функция, която намира сумата на две дадени матрици. 4. Да се дефинира функция diagonal, която извлича диагонала на квадратна

матрица. 5. Да се дефинира функция, която превръща даден списък в множество, т.е.

списък от неповтарящи се елементи. 6. Да се дефинира функция, която установява дали едно множество е

собствено подмножество на друго, т.е. първото множество е подмножество на второто и двете множества не са равни.

7. Да се дефинира функция powerset, която намира степенното множество на дадено множество (множеството от всички подмножества на разглежданото множество). Например, powerset [1, 2, 3] = [[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]].

8. Да се дефинира вариант на функцията cartprod така, че да не се използва оператора @.

9. Един начин да се получат простите числа е да се използва алгоритъма решето на Ератосфен. Започва се със списък съдържащ всички числа от 2 до n. Избира се първото число, което винаги е просто. После се отстраняват от останалите числа в списъка тези, които се делят на него. Това се повтаря върху резултата от предишната стъпка докато в списъка останат само прости числа. Да се дефинира функция prime, която реализира описания алгоритъм.

10. Две числа са прости близнаци ако те са прости числа и освен това тяхната разлика е 2. Да се дефинира функция, която намира броя на всички такива двойки прости числа по-малки от 1000.

11. Едно число е съвършенно ако сумата от всичките му делители е равна на самото число (1 се разглежда като делител, но не и самото число). Например, 6 е съвършенно, тъй като 1 + 2 + 3 = 6. Да се дефинира функция, която намира всички съвършенни числа по-малки от 1000.

12. Да се дефинира функция, която за дадено цяло число r намира всички двойки от цели числа (x, y) такива, че r = x2 + y2.

Page 47: Lectures FP

Въведение в стандарта ML

Функционално програмиране

47

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 6

ГРАФОВИ АЛГОРИТМИ И АЛГОРИТМИ ЗА СОРТИРАНЕ НА

СПИСЪЦИ

1. Насочени графи Списък от наредени двойки може да представя насочен граф. Всяка

двойка (x, y) замества дъгата x → y. Така списъка

− val graph = [(1, 2), (1, 3), (1, 4), (2, 6), (3, 5), (4, 6), (6, 7)];

> val graph = [(1, 2), (1, 3), (1, 4), (2, 6), (3, 5), (4, 6), (6, 7)] : int * int list представя графа 2 1 3 5 6 7

4

Сега ще дефинираме функцията nexts, която намира всички наследници

на даден връх, т.е. всички върхове до които има стрелки излизащи от

разглеждания връх.

− fun nexts (z, []) = [] | nexts (z, (x, y) :: xs) = if z = x then y :: nexts (z, xs) else nexts (z, xs); > val nexts = fn : ′′a * (′′a * ′b) list −> ′b list

Функцията nexts е полиморфна, както може да се забележи от отговора на ML системата. Единственото ограничение е типът на първата компонента на наредената двойка да допуска тест за равенство. Така, разглежданият връх последователно се сравнява с първата компонента на всяка наредена двойка представяща дъга в графа и при съвпадение втората компонента на двойката се добавя към списъка от наследници.

− nexts (6, graph); > val it = [5, 7] : int list

Page 48: Lectures FP

Въведение в стандарта ML

Функционално програмиране

48

Търсене в дълбочина Алгоритъмът за търсене в дълбочина е в основата на много графови

алгоритъми в които се налага обхождане на върховете на графа. Функцията depthfirst реализира този алгоритъм. Тя има три аргумента, а именно три списъка: първият списък съдържа върховете, които чакат да бъдат посетени, вторият списък е самият граф и третият е списъкът от обходените вече върхове, т.е. в него се акумулира резултата от функцията.

− fun depthfirst ([], graph, visited) = visited | depthfirst (x :: xs, graph, visited) = if x mem visited then depthfirst (xs, graph, visited) else depthfirst (nexts (x, graph) @ xs, graph, x :: visited); > val depthfirst = fn : ′′a list * (′′a * ′′a) list * ′′a list −> ′′a list

След посещаването на върха x, който не е в списъка visited от обходените вече върхове, търсенето в дълбочина посещава рекурсивно всеки наследник на x. В списъка, който се получава от израза nexts (x, graph) @ xs, наследниците на x предхождат останалите върхове в xs, които чакат да бъдат посетени. Този списък се държи като стек. Търсене в ширина ще получим ако списъка е реализиран като опашка.

− depthfirst ([1], graph, []); > val it = [4, 3, 7, 5, 6, 2, 1]

Топологична сортировка Ограниченията налагани върху реда на събитията формират насочен

граф. Всяка дъга x → y означава “x трябва да се случи преди y”. Например, следващият граф съдържа информация относно всичко което правим преди да тръгнем за работа.

тръгване

събуждане

измиване

закуска

обличане

душ

Page 49: Lectures FP

Въведение в стандарта ML

Функционално програмиране

49

Намирането на линейна последователност от върховете на такъв граф се нарича

топологична сортировка. Ако се върнем на търсенето в дълбочина

забелязваме, че x идва в списъка от обходените върхове след всеки връх

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

Следователно, с лека промяна на функцията depthfirst можем да реализираме

тази сортировка.

− fun topsort graph = let fun sort ([], visited) = visited | sort (x :: xs, visited) = sort (xs, if x mem visited then visited else x :: sort (nexts (x, graph), visited))

val (starts, _) = split graph in sort (starts, []) end;

> val topsort = fn : (′′a * ′′a) list −> ′′a list Тъй като, аргумента graph се подава непроменен при рекурсивните извиквания то в горната функция той е направен глобален за функцията sort. Освен това, чрез влагане на рекурсивното извикване на sort се избягва обръщение към оператора @. В локално декларираната променлива starts се съдържа списъка от началните върхове на дъгите за да се осигури, че всеки връх в графа е достижим. Функцията split се прилага върху списък от наредени двойки и връща наредена двойка от списъците съдържащи съответните компоненти на разделените двойки.

− fun split [] = ([], []) | split (pair :: pairs) = let fun conspair ((x, y), (xs, ys)) = (x :: xs, y :: ys) in conspair (pair, split pairs) end; > val split = fn : (′a * ′b) list −> ′a list * ′b list − split [(1, 2), (3, 4)]; > val it = ([1, 3], [2, 4]) : int list * int list

Нека сега приложим функцията topsort върху по-горе дадения граф graph за да видим разликата от depthfirst.

− topsort (graph); > val it = [1, 2, 6, 5, 7, 3, 4]

Page 50: Lectures FP

Въведение в стандарта ML

Функционално програмиране

50

2. Сортиране на списъци Сортирането е една от най-изучаваните области в изчислителната теория.

Алгоритмите за сортиране чрез вмъкване, сливане или бързата сортировка

(quick sort) са широко познати. Тези алгоритми обикновенно се прилагат върху

масиви. Например, за сортирането на масив от n елемента, сортирането чрез

вмъкване отнема от порядъка на n2 време. Очевидно е, че алгоритмите ще бъдат

по-бавни когато работят със списъци. Обаче тяхната времева сложност остава

непромененна, т.е. при прилагането на алгоритъма за сортираме чрез вмъкване

върху списък времето за изпълнение отново ще е пропорционално на n2.

Разбира се, при сортирането на списъци константата на пропорционалност ще е

по-висока.

Сортиране чрез вмъкване Сортирането чрез вмъкване се осъществява чрез вмъкване на елементи

един по един в сортиран списък. Функцията ins вмъква елемент в сортиран списък така, че списъкът да остане подреден.

− fun ins (x : real, []) = [x] | ins (x, y :: ys) = if x <= y then x :: y :: ys else y :: ins (x, ys); > val ins = fn : real * real list −> real list

В декларацията на горната функция изрично е зададен типа на вмъквания елемент за да се разреши проблема с претоварването на операторите за сравнение. Това се налага за всички сортиращи функции. Нека да тестваме функцията ins с вмъкването на елемент в едноелементен списък.

− ins (7.0, [9.0]); > val it = [7.0, 9.0] : real list

После към резултата може да добавим нов елемент и да получим отново сортиран списък.

− ins (8.0, it); > val it = [7.0, 8.0, 9.0] : real list

Функцията insort извиква ins за всеки елемент на входния списък.

Page 51: Lectures FP

Въведение в стандарта ML

Функционално програмиране

51

− fun insort [] = [] | insort (x :: xs) = ins (x, insort xs); > val ins = fn : real list −> real list

Горната функция не е ефективна, тъй като рекурсията е дълбока. Времето за изпълнение е от порядъка на n2. За списък от 10 000 случайно генерирани цели числа функцията insort отнема повече от 11 мунути. Сортирането чрез вмъкване може да се разглежда само за кратки списъци или такива които са почти сортирани.

Бърза сортировка Бързата сортировка е един от най-ефективните алгоритми за сортиране.

Тя се състои в следното:

• Избиране на стойност x от входа наречена ос.

• Разделяне на останалите елементи на две части: елементи по-малки от x и елементи по-големи от x.

• Сортиране на всяка част рекурсивно. След това частта с по-малките елементи се поставя преди частта с по-големите.

Бързата сортировка е идеална за масиви. Разделящата стъпка е много бърза. При списъци се налага да се копират всички елементи. Следващата функция реализира бързата сортировка за списъци. Локалната функция partition извършва разделянето на списъка на две части и е итеративна функция връщаща два резултата.

− fun quick [] = [] | quick [x] = [x] | quick (x :: xs) : real list =

let fun partition ([], left, right) = (quick left) @ (x :: quick right) | partition (y :: ys, left, right) = if x < y then partition (ys, left, y :: right) else partition (ys, y :: left, right) in partition (xs, [], []) end; > val quick = fn : real list −> real list

Тази функция сортира списък от 10 000 случайно генерирани цели числа за 6 секунди. Тя отнема от порядъка на nlogn време в средния случай. Докато когато елементите в списъка са в низходящ или възходящ ред времето е от порядъка на n2.

Page 52: Lectures FP

Въведение в стандарта ML

Функционално програмиране

52

Сортиране чрез сливане Сливането на подредени списъци става чрез повторно вземане на по-

малката от главите на два сортирани списъка. Това се повтаря докато елементите на единия от двата списъка се изчерпят. Тогава останалите елементи от по-дългия списък просто се копират в края на резултатния списък. Този алгоритъм се реализира от следващата функция:

− fun merge ([], ys) = ys | merge (xs, []) = xs | merge (x :: xs, y :: ys) : real list = if x <= y then x :: merge (xs, y :: ys) else y :: merge (x :: xs, ys); > val merge = fn : real list * real list −> real list Няколко алгоритъма работят чрез сливане на подредени списъци.

Например, списък може да бъде сортиран чрез разделяне на елементите му в два списъка с приблизително равна дължина. Тези списъци след това се сортират рекурсивно и резултатите се сливат. За разделянето на списъка се използват функциите take и drop. Този сортиращ алгоритъм е известен като сортиране чрез сливане от върха към дъното.

− fun mergesort [] = [] | mergesort [x] = [x] | mergesort xs = let val n = length xs div 2 in merge (mergesort (take (n, xs)), mergesort (drop (n, xs))) end; > val mergesort = fn : real list −> real list

Функцията mergesort сортира 10 000 случайно генерирани цели числа за 7 секунди. Дори и в най-лошия случай, за разлика от бързото сортиране, времевата сложност е от порядъка на nlogn.

Друг начин на сортиране чрез сливане е от дъното към върха. При този алгоритъм списъка се разглежда като списък от едноелементни списъци. Тези списъци се групират два по два и се сливат. Така се получават подредени списъци с дължина две, които отново се групират и сливат в списъци с дължина 4 и т.н., това продължава докато се получи един сортиран списък. Тук обаче, ще разгледаме алгоритъм в който се използва естествената подредба на елементите, т.е. вместо да разделяме списъка на едноелементни списъци ще го

Page 53: Lectures FP

Въведение в стандарта ML

Функционално програмиране

53

разделяме на списъци от подредени серии. Функцията nextrun връща следващата подредена серия от даден списък и списъка от останалите елементи.

− fun nextrun (run, []) = (rev run, [] : real list) | nextrun (run, x :: xs) = if x < hd run then (rev run, x :: xs) else nextrun (x :: run, xs); > val nextrun = fn : real list * real list −> real list * real list

Тъй като, елементите в серията run са в обратен ред е необходимо да се извика функцията rev.

− val l = [2.0, 5.0, 18.0, 3.0, 1.0, 15.0, 2.0]; > val l = [2.0, 5.0, 18.0, 3.0, 1.0, 15.0, 2.0] : real list; − nextrun ([hd l], tl l); > val it = ([2.0, 5.0, 18.0], [3.0, 1.0, 15.0, 2.0]) : real list * real list

В дефиницията на функцията nextrun е използвана функцията hd за да се отнесем към главата на списъка run. Вместо това можем да използваме образеца: run as r :: _ . Така чрез r можем да се обърнем към главата на run. Отбелязваме, че run и r :: _ представят един и същи списък. Такъв тип образци се наричат напластени (layered). Тогава, дефиницията на функцията nextrun ще изглежда като:

− fun nextrun (run, []) = (rev run, [] : real list) | nextrun (run as r :: _, x :: xs) = if x < r then (rev run, x :: xs) else nextrun (x :: run, xs); Нека да разгледаме функцията mergeruns. Тя има два аргумента: списък

от списъци и цяло число k. Когато цялото число k е четно тогава в началото на списъка има два подредени списъка за сливане. За k = 0, mergeruns слива целия списък от списъци в един списък.

− fun mergeruns ([l], k) = [l] | mergeruns (l1 :: l2 :: ls, k) = if k mod 2 = 1 then l1 :: l2 :: ls else mergeruns (merge (l1, l2) :: ls, k div 2); > val mergeruns = fn : real list list * int −> real list list

Функцията mergeruns се използва в следващата функция sorting, която многократно взема серии от списъка и ги слива. Тя има три аргумента: списъка който се сортира, помощен списък в който се поставят намерените серии и цяло число което определя кога има две серии за сливане.

Page 54: Lectures FP

Въведение в стандарта ML

Функционално програмиране

54

− fun sorting ([], ls, k) = hd (mergeruns (ls, 0)) | sorting (x :: xs, ls, k) = let val (run, tail) = nextrun ([x], xs) in sorting (tail, mergeruns (run :: ls, k+1), k+1) end; > val sorting = fn : real list * real list list * int −> real list

Основната сортираща функция е smoothsort. − fun smoothsort [] = [] | smoothsort xs = sorting(xs, [], 0); > val smothsort = fn : real list −> real list

Разгледаният алгоритъм е едновременно елегантен и ефективен. Той сортира 10 000 случайно генерирани цели числа за 5 секунди. Накрая нека да разгледаме вариант на функцията merge с използването на

case израза в ML.

− fun merge (xlist, []) = xlist | merge (xlist, ylist as y :: ys) : real list = case xlist of [] => ylist | x :: xs => if x <= y then x :: merge (xs, ylist) else y :: merge (xlist, ys); > val merge = fn : real list * real list −> real list

Забелязваме, че в горната дефиниция xlist и x :: xs означават един и същи израз, т.е. ефект, който се получава при напластените образци. Case израза е използван за да се тества първия аргумент на функцията. Той е още едно средство за съпоставяне на образци и има вида case E of P1 => E1 | … | Pn => En

Стойността на израза E се съпоставя срещу образците P1,..., Pn и ако Pi е

първият образец, който съответства тогава резултата е стойността на израза Ei.

Следователно case израза е еквивалентен на функция дефинирана чрез случаи и

приложена върху израза E.

Page 55: Lectures FP

Въведение в стандарта ML

Функционално програмиране

55

Задачи за упражнение

1. Да се дефинира функция, която обхожда всички върхове на граф, чрез търсене в ширина.

2. Да се дефинира функция, която проверява дали има път между два върха на граф, представен като списък от наредени двойки.

3. Да се дефинира функция, която проверява дали има цикъл в даден граф, представен като списък от наредени двойки, и връща върховете участващи в него.

4. Да се дефинира функция, която намира медианата на даден списък, т.е. стойност за която е изпълнено, че броя на елементи по-малки от нея е равен на броя на тези които са по-големи.

5. Да се предефинира функцията quick така, че в новата дефиниция да не се използва оператора @, т.е. да се използва втори аргумент в който да се акумулира сортирания списък (quick (x :: xs, sorted)).

6. Да се дефинира функция find такава, че find(l, i) връща i-я най-малък елемент в списъка l.

7. Да се дефинира функция findrange, която обобщава функцията find такава, че findrange(l, i, j) връща списък от i-я до j-я най-малки елементи в списъка l.

8. Да се дефинира функция, която връща началната позиция и дължината на първата срещната последователност от еднакви числа в сортиран списък.

9. Да се дефинира функция предикат, която установява дали списък от цели числа е сортиран.

10. Да се дефинира функция, която сортира списък от цели числа по метода на мехурчето, т.е. обхожда елементите на списъка от началото до края като ги сравнява два по два и ги разменя ако не са разположени в нарастващ ред. Това се повтаря докато списъка се сортира.

11. Да се дефинира функция, която намира едновременно нарастващи и намаляващи серии от елементи в даден списък.

Page 56: Lectures FP

Въведение в стандарта ML

Функционално програмиране

56

Page 57: Lectures FP

Въведение в стандарта ML

Функционално програмиране

57

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 7

ДЕФИНИРАНЕ НА ПОТРЕБИТЕЛСКИ ТИПОВЕ

– TYPE И DATATYPE ДЕКЛАРАЦИИ.

ИЗКЛЮЧЕНИЯ. ДЪРВЕТА.

1. Type и datatype декларации Векторите са наредени двойки от реални числа. Те могат да бъдат както

аргументи така и резултати от функции. Освен това е възможно да бъде

деклариран тип вектор:

− type vec = real * real; > type vec = real * real

Сега vec е съкращение на real × real. Отбелязваме, че името на типа въведен чрез type декларацията може да бъде използвано при изричното задаване на типове във функционалните дефиниции. Обаче, ML системата не използва това име. Например, да разгледаме функцията addvec, която събира два вектора:

− fun addvec ((x1, y1), (x2, y2)) : vec = (x1 + x2, y1 + y2); > val addvec = fn : (real * real) * (real * real) −> real * real

Очевидно, ML системата не съкращава всеки тип real × real като vec. Datatype декларацията въвежда напълно нов тип данни в програмата. Да

предположим, че искаме да представим седемте дни от седмицата. За да направим това трябва да дефинираме тип данни DAY.

− datatype DAY = Mon | Tue | Wed | Thu | Fri | Sat | Sun; > datatype DAY = Mon | Tue | Wed | Thu | Fri | Sat | Sun

con Mon = Mon : DAY

con Tue = Tue : DAY

con Sun = Sun : DAY

В горната декларация осем нови идентификатори са въведени: името на типа и

седем конструктори, които са означени с думата con. Типове данни от този вид

Page 58: Lectures FP

Въведение в стандарта ML

Функционално програмиране

58

се наричат изброими, тъй като се дефинират чрез изброяване на своите

елементи.

След деклариране на типа DAY, ML системата знае за новите обекти и

може да разреши коректно техния тип.

− Wed;

> val it = Wed : DAY

За изброимите типове единствените валидни операции в ML са проверките за

равенство (=) и различие (< >). Чрез дефинирането на функции е възможно да

се разшири множеството от допустимите операциите. Така следващата функция

установява дали ден от седмицата е работен.

− fun is_workingday day = day <> Sat andalso day <> Sun; > val is_workingday = fn : DAY −> bool Отбелязваме, че стандартният тип bool е изброим тип с конструктори true

и false и е дефиниран чрез − datatype bool = true | false;

2. Деклариране и обработване на изключения Изключенията са тип данни от грешни стойности, които се третират

специално за да се намали изричното тестване. Когато изключение е вдигнато

то се предава чрез всички ML функции докато то бъде открито и обработено.

ML има няколко стандартни изключения основно за грешки от

стандартните функции. Те са следните:

Ln се вдига от ln(x) за x ≤ 0

Sqrt се вдига от sqrt(x) за x < 0

Ord се вдига от ord(s) когато s е празен низ

Chr се вдига от chr(k) ако k < 0 или к > 255

Interrupt се вдига от програмата когато е прекъсната външно

Io сигнализира за грешки през време на входните и изходните операции

Page 59: Lectures FP

Въведение в стандарта ML

Функционално програмиране

59

Стандартните изключения Match или Bind се вдигат когато съпоставянето на

образци е неуспешно. Функция вдига изключението Match когато се прилага

към аргумент несъответстващ на никои от нейните образци. Ако case израза

няма образец който да съответства на селектора също се вдига Match. Тъй като,

много функции могат да вдигнат Match, това изключение носи малко

информация.

Основните функции над списъци hd и tl, дефинирани в лаб. упр. 4, могат

да бъдат декларирани с изключения Hd и Tl за да се указва когато се прилагат

погрешно, т.е. върху празен списък.

− exception Hd;

> exn Hd = Hd : exn

− fun hd (x :: _) = x | hd [] = raise Hd; > val hd = fn : ′a list −> ′a − exception Tl;

> exn Tl = Tl : exn

− fun tl (_ :: xs) = xs | tl [] = raise Tl; > val tl = fn : ′a list −> ′a list

Така когато приложим функцията hd към празен списък ще бъде вдигнато предварително декларираното изключение Hd.

− hd []; ! Uncaught exception: ! Hd

Нека сега да разгледаме функция nth, която връща n-тия елемент на списък.

Първо ще декларираме изключението Nth, което ще бъде използвано в

дефиницията на функцията за да сигнализира когато тя се прилага върху

некоректни аргументи.

− exception Nth;

> exn Nth = Nth : exn

Page 60: Lectures FP

Въведение в стандарта ML

Функционално програмиране

60

− fun nth (x :: _, 1) = x | nth (x :: xs, n) = if n > 0 then nth (xs, n-1) else raise Nth | nth _ = raise Nth; > val nth = fn : ′a list * int −> ′a

Горната функция вдига изключението Nth ако n ≤ 0 или ако списъкът няма n-ти елемент.

− nth ([1, 2, 3], 4); ! Uncaught exception: ! Nth

Изключенията се обработват с помощта на манипулатора на

изключенията. Той прилича на case конструктора:

E handle P1 => E1 | … | Pn => En

Резултата от всеки израз се тества дали е стойност от тип exn. Така, ако E върне

нормална стойност тогава манипулатора просто я пропуска. От друга страна,

ако E върне стойност от тип exn то тя се съпоставя срещу образците. Ако Pi е

първият образец който съответства тогава стойността на израза Ei се връща като

резултат на израза E. Нека като пример да разгледаме следващата функция, която изчислява

сумата от елементите на списък от позиции i, f(i), f(f(i)), …. В дефиницията на

функцията sumchain е използвана описаната по-горе функция nth. Така, при

срещането на позиция извън областта сумирането прекъсва и се вдига

изключението Nth. То се открива и обработва от манипулатора на изключенията

като нулата се връща като стойност на рекурсивното извикване на локалната

функция sum.

− fun sumchain (l, n, f) = let fun sum i =

nth(l, i) + sum(f(i)) handle Nth => 0 in sum n end; > val sumchain = fn : int list * int * (int −> int) −> int

Page 61: Lectures FP

Въведение в стандарта ML

Функционално програмиране

61

3. Дървета Дървото е разклонена структура, която се състои от върхове с

разклонения водещи към поддървета. Във върховете могат да се пазят

стойности наричани етикети. Листата са неетикирани. Двоичното дърво има

разклонени връхове с етикети и две поддървета. Двоичните дървета в ML се

дефинират чрез рекурсивни datatype декларации.

− datatype ′a TREE = Lf

| Node of ′a * ′a TREE * ′a TREE;

Типът ′a TREE се състои от всички стойности (дървета), които могат да бъдат

построени чрез конструкторите Lf и Node. Lf е базовият случай на рекурсията и

е синоним на празно дърво.

Нека да видим как дърветата могат да се комбинират за да се получи по-

голямо дърво. Да разгледаме дървета с етикети цели числа.

− val tree2 = Node(2, Node(1, Lf, Lf), Node(3, Lf, Lf)); > val tree2 = Node(2, Node(1, Lf, Lf), Node(3, Lf, Lf)) : int TREE − val tree6 = Node(6, Node(5, Lf, Lf), Node(7, Lf, Lf)); > val tree6 = Node(6, Node(5, Lf, Lf), Node(7, Lf, Lf)) : int TREE − val tree4 = Node(4, tree2, tree6);

Така двоичното дърво tree4 има в корена си етикет 4, а за ляво и дясно поддървета му служат съответно дърветата tree2 и tree6.

Операциите върху дървета се описват чрез рекурсивни функции със

съпоставяне на образци. За празно дърво се използва образеца Lf, докато за непразно се използва конструктора Node, т.е. Node(v, t1, t2) представя двоично дърво с корен v, а t1 и t2 са съответно образците за лявото и дясното му поддървета. Следващата функция намира броя на върховете в двоично дърво.

4

6

1 3 5 7

2

Page 62: Lectures FP

Въведение в стандарта ML

Функционално програмиране

62

− fun count Lf = 0

| count (Node(v, t1, t2)) = 1 + count t1 + count t2;

> val count = fn : ′a TREE −> int

− count tree4;

> val it = 7 : int

Друга мярка за размера на дървото е неговата дълбочина, т.е. дължината на най-

дългия път от корена до лист. В дефиницията на функцията depth е използвана

функцията max, която намира по-голямото от две цели числа.

− fun depth Lf = 0

| depth (Node(v, t1, t2)) = 1 + max (depth t1, depth t2);

> val depth = fn : ′a TREE −> int

− depth tree4;

> val it = 3 : int

4. Списъци и дървета Да разгледаме задачата за обхождане на върховете на двоично дърво, т.е.

получаване на списък от неговите етикети. Има три добре познати начина за

това: префиксен, инфиксен и постфиксен, които могат да се опишат с

рекурсивни функции. Ако разгледаме разклонен връх винаги етикетите от

лявото поддърво се поставят преди тези в дясното. Трите начина се различават

само по това къде се поставя етикета на върха. При префиксното обхождане

етикета се поставя преди етикетите на поддърветата.

− fun preorder Lf = []

| preorder (Node(v, t1, t2)) = [v] @ preorder t1 @ preorder t2;

> val preorder = fn : ′a TREE −> ′a list

− preorder tree4;

> val it = [4, 2, 1, 3, 6, 5, 7] : int list

При инфиксното обхождане етикета се поставя между етикетите на лявото и

дясното поддървета.

Page 63: Lectures FP

Въведение в стандарта ML

Функционално програмиране

63

− fun inorder Lf = []

| inorder (Node(v, t1, t2)) = inorder t1 @ [v] @ inorder t2;

> val inorder = fn : ′a TREE −> ′a list

− inorder tree4;

> val it = [1, 2, 3, 4, 5, 6, 7] : int list

Аналогично може да се дефинира функцията, която извършва постфиксно

обхождане, т.е. поставя етикета накрая. Горните функции са ясни, но заемат

квадратно време за небалансирани дървета. Причината за това е използването

на оператора @. Ние можем да го елиминираме като използваме допълнителен

аргумент vs за резултата. Нека да предефинираме горните две функции.

− fun preord (Lf, vs) = vs

| preord (Node(v, t1, t2), vs) = v :: preord (t1, preord (t2, vs));

− fun inord (Lf, vs) = vs

| inord (Node(v, t1, t2), vs) = inord (t1, v :: inord (t2, vs));

Сега да разгледаме обратната операция, а именно превръщане на списък

от етикети в дърво. Понятията префиксен, инфиксен и постфиксен се прилагат

и за тази обратна операция. Един списък може да бъде превърнат в много

различни дървета. Обаче, единствено интересно е получаването на балансирано

дърво. Следващата функция построява балансирано дърво като взема етикета

от началото на списъка. В дефиницията на функцията balpreord са използвани

функциите take и drop (виж лаб. упр. 4) за да се разделят елементите в списъка

по-равно в дясното и лявото поддървета.

− fun balpreord [] = Lf

| balpreord (x :: xs) =

let val k = length xs div 2

in Node(x, balpreord (take(k, xs)), balpreord (drop(k, xs))) end;

> val balpreord = fn : ′a list −> ′a TREE

Ако приложим функцията balpreord към списъка [1, 2, 3, 4, 5, 6, 7] ще получим

балансирано двоично дърво.

Page 64: Lectures FP

Въведение в стандарта ML

Функционално програмиране

64

− balpreord [1, 2, 3, 4, 5, 6, 7];

> val it = Node(1, Node(2, Node(3, Lf, Lf), Node(4, Lf, Lf)), > Node(5, Node(6, Lf, Lf), Node(7, Lf, Lf))) : int TREE

За да се направи балансирано дърво от инфиксен списък етикета се взема от

средата на списъка.

− fun balinord [] = Lf

| balinord xs =

let val k = length xs div 2

val y :: ys = drop (k, xs)

in Node(y, balinord (take(k, xs)), balinord ys) end;

> val balinord = fn : ′a list −> ′a TREE

Когато приложим горната функция към сортиран списък получаваме

балансирано двоично търсещо дърво (виж лаб. упр. 8).

− balinord [1, 2, 3, 4, 5, 6, 7];

> val it = Node(4, Node(2, Node(1, Lf, Lf), Node(3, Lf, Lf)), > Node(6, Node(5, Lf, Lf), Node(7, Lf, Lf))) : int TREE

Page 65: Lectures FP

Въведение в стандарта ML

Функционално програмиране

65

Задачи за упражнение

1. Двоичното дърво t е пълно ако count(t) ≤ 2depth(t) – 1. Да се състави функция построяваща пълно двоично дърво с дълбочина n етикираща върховете с целите числа от 1 до 2n.

2. Да се дефинира функция reflect, която формира огледално изображение на дадено двоично дърво чрез разменяне на дясното и лявото му поддървета.

3. Да се дефинира функция, която проверява дали дадено двоично дърво е симетрично относно корена си.

4. Двоично дърво t е балансирано ако за всеки връх е изпълнено |count(t1) - count(t2)| ≤ 1, където t1 и t2 са съответните поддървета. Да се дефинира функция, която проверява дали дадено двоично дърво е балансирано.

5. Да се дефинира функция, която проверява дали двоичните дървета t1 и t2 са огледални, т.е. t1 = reflect(t2).

6. Да се даде datatype декларация на тип еквивалент на ′a list. 7. Да се дефинира тип на двоично дърво (′a, ′b) TREE където върховете

съдържат етикети от тип ′a, а листата етикети от тип ′b. 8. Да се дефинира тип на дърво в което всеки връх може да има произволен

краен брой поддървета. 9. Да се дефинира функция, която обхожда дадено двоично дърво в

постфиксен ред. 10. Да се дефинира функция която построява балансирано двоично дърво от

зададен списък от цели числа като етикета за корена се взема от края на списъка.

11. Да се дефинира функция, която проверява дали даден етикет се появява в дадено двоично дърво.

12. Да се дефинира функция, която пресмята броя на листата в дадено двоично дърво.

13. Да се дефинира функция, която връща отляво надясно етикетите на онези върхове на дадено двоично дърво, които нямат поддървета.

Page 66: Lectures FP

Въведение в стандарта ML

Функционално програмиране

66

Page 67: Lectures FP

Въведение в стандарта ML

Функционално програмиране

67

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 8

ДВОИЧНИ ТЪРСЕЩИ ДЪРВЕТА.

ФУНКЦИОНАЛНИ МАСИВИ И ПРИОРИТЕТНИ

ОПАШКИ

1. Двоични търсещи дървета Двоичните търсещи дървета са доста по-ефективни за съхраняване на

информация отколкото списъците. Разбира се това е валидно само в случая

когато са разумно балансирани. Времето за намиране на информация по

зададен ключ сред n елемента е от порядък n за списъци и от порядък logn за

двоични търсещи дървета. Времето за обновяване на дърво е също от порядък

logn. Разбира се, в най-лошия случай двоичните търсещи дървета могат да

бъдат толкова бавни колкото и списъците, защото понякога последователното

добавяне на елементи може да доведе до силно небалансирани дървета.

Ключовете при списъци могат да бъдат от всеки тип допускаш тест за

равенство докато ключовете в двоичните търсещи дървета трябва да допускат

линейно подреждане. Така ние ще разглеждаме двоични търсещи дървета на

които всеки връх е наредената двойка (низ, стойност), т.е. типа на ключовото

поле е символен низ. Освен това за всеки връх е в сила, че лявото му поддърво

съдържа в ключовото си поле по-малък низ, а съответно дясното поддърво по-

голям низ. Инфиксното обхождане на дървото дава подреден по азбучен ред

списък (виж лаб. упр. 7).

По-надолу е даден пример на двоично търсещо дърво, което е

балансирано и съдържа 5 елемента. Три операции са дефинирани за двоичните

търсещи дървета: търсене, обновяване и вмъкване. Нека сега да видим как ще

изглеждат ML функциите, които ги реализират.

Page 68: Lectures FP

Въведение в стандарта ML

Функционално програмиране

68

При операциите за търсене и вмъкване може да се получи грешка. За това

нека да декларираме следните изключения:

− exception Lookup;

− exception Insert;

Търсенето в двоично търсещо дърво е просто. То винаги започва от корена.

Даденият ключ се сравнява с ключа на текущия връх и ако е по-малък от него

търсенето продължава в лявото поддърво ако е по-голям в дясното поддърво.

Ако елемент с дадения ключ не е намерен се вдига изключението Lookup.

Функцията lookup реализира описания алгоритъм за търсене.

− fun lookup (Node ((a, x), t1, t2), b: string) =

if b < a then lookup (t1, b)

else if a < b then lookup (t2, b) else x

| lookup (Lf, b) = raise Lookup;

> val lookup = fn : (string * ′a) TREE * string −> ′a

Вмъкването на елемент в двоично търсещо дърво включва намиране на

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

вмъкване на стойността. Ако в дървото вече има елемент с такъв ключ се вдига

изключението Insert.

− fun insert (Lf, b: string, y) = Node ((b, y), Lf, Lf)

| insert (Node ((a, x), t1, t2), b, y) =

if b < a then Node ((a, x), insert (t1, b, y), t2)

else if a < b then Node ((a, x), t1, insert (t2, b, y)) else raise Insert;

Hungary, 36

Mexico, 52 France, 33

Japan, 81 Egypt, 28

Page 69: Lectures FP

Въведение в стандарта ML

Функционално програмиране

69

> val insert = fn : (string * ′a) TREE * string * ′a −> (string * ′a) TREE

Функцията, която реализира обновяването на стойността на даден елемент

може да се опише аналогично с тази разлика, че се търси елемент по даден

ключ, чиято стойност ще бъде променена. Отбелязваме, че операциите за

вмъкване и обновяване не модифицират текущото дърво, а създават ново. Това

не е толкова неефективно тъй като, новото дърво споделя по-голяма част от

своята памет със съществуващото.

Двоично търсещо дърво се построява като се започне от празно дърво и

последователно се вмъкват елементи. Нека да коструираме дърво tree1, което

съдържа Hungary и France.

− insert (Lf, "Hungary", 36);

> Node(("Hungary", 36), Lf, Lf) : (string * int) TREE

− val tree1 = insert (it, "France", 33);

> val tree1 = Node(("Hungary", 36), Node(("France", 33), Lf, Lf), Lf) :

(string * int) TREE

Аналогично могат да бъдат вмъкнати върховете за Mexico, Egypt и Japan. Така

ще получим по-горе изобразеното двоично търсещо дърво. После като

приложим функцията inorder получаваме сортиран по азбучен ред списък.

− inorder (tree1);

> [("Egypt", 28), ("France", 33), ("Hungary", 36), ("Japan", 81),

("Mexico", 52)] : (string * int) list

2. Функционални масиви Функционалните масиви задават съответствие между крайно

подмножество на естествените числа и множество от стойности. Те могат да

бъдат реализирани чрез двоични дървета. Позицията на запис k в дървото се

намира като се тръгне от корена и k се дели на 2 докато се редуцира до 1. Всеки

път когато остатъка е 0 преминаваме в лявото поддърво, ако остатъка е 1

Page 70: Lectures FP

Въведение в стандарта ML

Функционално програмиране

70

продължаваме в дясното поддърво. Например, в по-долу изобразеното дърво

запис 12 се достига чрез ляво, ляво, дясно:

Това представяне позволява масива да расте и намалява. Двоичното дърво е

винаги балансирано. Търсенето и обновявянето на запис k отнема от порядъка

на logk стъпки.

Нека да предположим, че елемент n е дефиниран само ако записи от 1 до

n-1 са вече дефинирани. Горна граница на масива е най-високата дефинирана

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

граница или да се свива съответно като я намалява. Долната граница е

фиксирана на 1. Основните операции върху функционални масиви са:

Търсене: връща стойността пазена в дадена позиция на масива;

Обновяване: заменя стойността пазена в дадена позиция на масива с

нова такава, понякога увеличава горната граница;

Изтриване: намалява горната граница на масива като изтрива записа от

най-високата позиция.

ML функциите реализиращи горните операции приличат на съответните

за двоични търсещи дървета. Функцията за търсене flookup дели k на 2 докато

се стигне до 1. Ако остатъка е 0 тогава функцията следва лявото поддърво,

иначе дясното поддърво. При достигане на лист се вдига декларираното

изключение Array.

− exception Array;

1

3

4 6 5 7

2

8 12 10 14 9 13 11 15

Page 71: Lectures FP

Въведение в стандарта ML

Функционално програмиране

71

− fun flookup (Lf, _) = raise Array

| flookup (Node(v, t1, t2), k) =

if k = 1 then v

else if k mod 2 = 0 then flookup (t1, k div 2)

else flookup (t2, k div 2);

> val flookup = fn : ′a TREE * int −> ′a

Функцията за обновяване също дели повторно позицията на записа на 2.

Когато се стигне до 1 етикета на текущия връх получава нова стойност.

Възможно е също листо да бъде подменено с връх като по-този начин масива се

разширява. Това е първото уравнение в следващата функция, което има за

аргумент образеца Lf, т.е. празно дърво. В този случай ако k е равно на 1 се

добавя нов връх в дървото иначе се вдига изключението Array.

− fun fupdate (Lf, k, w) = if k =1 then Node (w, Lf, Lf)

else raise Array

| fupdate (Node(v, t1, t2), k, w) =

if k = 1 then Node (w, t1, t2)

else if k mod 2 = 0 then Node (v, fupdate (t1, k div 2, w), t2)

else Node (v, t1, fupdate (t2, k div 2, w));

> val fupdate = fn : ′a TREE * int * ′a −> ′a TREE

Резултата от функцията hirem (t, n) е изтриване на запис от позиция n в дървото

t. Изключение се вдига ако такъв елемент несъществува. Функцията е валидна

само когато n е горната граница на масива. Обаче, hirem не проверява това. Тя

дели n на 2 докато се редуцира до 1. Тогава съответният връх се заменя с листо.

− fun hirem (Lf, n) = raise Array

| hirem (Node(v, t1, t2), n) =

if n = 1 then Lf

else if n mod 2 = 0 then Node (v, hirem (t1, n div 2), t2)

else Node (v, t1, hirem (t2, n div 2));

> val hirem = fn : ′a TREE * int −> ′a TREE

Page 72: Lectures FP

Въведение в стандарта ML

Функционално програмиране

72

3. Приоритетни опашки Приоритетна опашка е подредена съвкупност от елементи, в която най-

големият елемент е най-лесно достъпен. Тя поддържа следните операции:

Добавяне: елемент се добавя към опашката;

Извеждане: най-големият елемент се извежда;

Изтриване: най-големият елемент се изтрива от опашката.

Ако опашката се пази като обърнат сортиран списък добавянето на елемент

отнема до n стъпки за опашка от n елемента. Това е твърде бавно. Ако се

използва двоично дърво, добавянето и изтриването отнемат от порядъка на logn

стъпки. Такова дърво, наречено пирамида, е ключа към добре известния

алгоритъм за сортиране пирамидална сортировка. Етикетите са подредени така,

че най-големият е винаги в корена на дървото.

Тъй като, искаме пирамидата да е с минимална дълбочина ще използваме

за индексирането на елементите индексната схема от функционалните масиви.

Така, ако пирамидата съдържа n-1 елемента тогава новият елемент запълва

позиция n. Обаче, този елемент може да е твърде голям за да отиде на позиция n

без да наруши условието на пирамидата, а именно най-големият елемент да е в

корена. Функцията upheap добавя нов елемент в позиция n като едновременно с

това поддържа споменатото условие.

− fun upheap (Lf, n, w: real) = Node (w, Lf, Lf)

| upheap (Node(v, t1, t2), n, w) =

if v > w then

if n mod 2 = 0 then Node (v, upheap (t1, n div 2, w), t2)

else Node (v, t1, upheap (t2, n div 2, w))

else (* w>=v *)

if n mod 2 = 0 then Node (w, upheap (t1, n div 2, v), t2)

else Node (w, t1, upheap (t2, n div 2, v));

> val upheap = fn : real TREE * int * real −> real TREE

Page 73: Lectures FP

Въведение в стандарта ML

Функционално програмиране

73

Операцията за изтриване трябва да освободи позиция n за да запази дървото

балансирано когато изтрива най-големия елемент от пирамидата. Обаче,

елемента от позиция n може да е твърде малък за да бъде поставен в корена на

дървото. За това елемента се движи надолу по дървото като на всеки връх

следва поддървото с по-голям етикет. Това спира когато няма поддърво

съдържащо по-голям етикет. Функцията downheap реализира този алгоритъм.

Използван е оператора case за да се изследват етикетите на дясното и лявото

поддървета.

− fun downheap (Node(_ , t1, t2), w: real) =

case t1 of

Lf => Node(w, Lf, Lf)

| Node(vl, _, _) =>

(case t2 of

Lf => if w > vl then Node(w, t1, Lf)

else Node(vl, downheap(t1, w), Lf)

| Node(vr, _, _) =>

if vl >= vr then if w > vl then Node(w, t1, t2)

else Node(vl, downheap(t1, w), t2)

else if w > vr then Node(w, t1, t2)

else Node(vr, t1, downheap(t2, w)));

> val downheap = fn : real TREE * real −> real TREE

Функцията dequeue, дефинирана по-долу, извиква flookup за да получи елемента

от позиция n, а hirem за да изтрие елемента. После downheap за да постави

елемента на точното място.

− exception Heap; − fun dequeue (hp, n) = if n > 1 then (downheap (hirem(hp, n), flookup(hp, n)), n-1) else if n = 1 then (Lf, 0) else raise Heap; > val dequeue = fn : real TREE * int −> real TREE * int

Page 74: Lectures FP

Въведение в стандарта ML

Функционално програмиране

74

Задачи за упражнение

1. Да се състави функция, която обновява двоично търсещо дърво с двойката (низ, стойност). Ако в дървото не съществува такъв елемент то той се добавя, иначе се обновява съществуващия с новата стойност.

2. Да се състави функция, която намира и изтрива елемент по зададен ключ в двоично търсещо дърво.

3. Да се състави функция, която построява двоично търсещо дърво от зададен списък от цели числа.

4. Да се състави функция, която проверява дали дадено двоично дърво е балансирано и ако не е го балансира.

5. Да се дефинира функция, която създава функционален масив от списъка [x1, x2, …, xn].

6. Да се състави функция, която обръща функционален масив състоящ от стойности x1, x2, …, xn (записи с позиции от 1 до n) в списък.

7. Да се дефинира функция upheaplist, която обръща списък l в пирамида (hp, n), където n е горната граница на функционалния масив представящ пирамидата.

8. Да се дефинира функция, която повторно взема най-големия елемент от пирамида за да построи сортиран списък.

Page 75: Lectures FP

Въведение в стандарта ML

Функционално програмиране

75

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 9

ФУНКЦИИ ОТ ПО-ВИСОК РЕД. БЕЗКРАЙНИ

СПИСЪЦИ И ОТЛОЖЕНИ ПРЕСМЯТАНИЯ

1. Функции от по-висок ред Функция може да бъде аргумент на друга функция или също резултат от

прилагане на функция. Функции с едното или и двете споменати свойства се

наричат функции от по-висок ред или по-кратко функционали. С тяхна помощ

е възможно да се дефинират по-общи функции, които могат да се използват за

различни приложения. Обикновенно функциите от по-висок ред са също и

полиморфни.

Анонимни функции с fn Функциите в ML не е необходимо да имат имена. Ако x е променлива от

тип a и E е израз от тип b тогава израза fn x => E

означава функция от тип a −> b, т.е. x е аргумента на функцията, а E е тялото. Освен това, ML синтаксиса позволява съпоставяне на образции:

fn P1 => E1 | … | Pn => En т.е. горният израз означава функция дефинирана чрез образците P1, …, P2. Нека като пример да разгледаме функция, която удвоява цяло число.

− (fn n => n * 2)(9)

> val it = 18 : int − val double = fn n => n * 2; > val double = fn : int −> int

Частично приложими функции Функциите могат да имат само един аргумент. Функции с многочислени

аргументи се разглеждат като функции с наредена n-торка за аргумент.

Page 76: Lectures FP

Въведение в стандарта ML

Функционално програмиране

76

Многочислени аргументи могат също да се реализират чрез функция, която връща друга функция като свой резултат. Нека да разгледаме функцията:

− fun add x y : int = x + y;

> val add = fn : int −> int −> int Стрелката −> е привързана надясно и типа на функцията add е int −>( int −> int). Следователно, функцията add взема цяло число като аргумент и връща целочислена функция като резултат. Нека сега да използваме факта, че add е частично приложима функция и да дефинираме функцията наследник чрез нея.

− val succ = add 1;

> val succ = fn : int −> int − succ 10; > val it = 11 : int

Отбелязваме, че не е необходимо да се въвежда име за функцията, която е резултат от прилагането на add.

− (add 10) 5; > val it = 15 : int

Частично приложимите функции са удобни за използване. Те дават възможност за съставяне на мощни функции, които могат да се използват за различни приложения.

Функционали върху списъци: map и filter Функционала map прилага функция върху всеки елемент на списък и

връща списък от функционални резултати, т.е. map f [x1, x2, …, xn] = [ f x1, f x2, …, f xn ]

Това е стандартна функция в ML, която може да бъде дефинирана както следва: − fun map f [] = []

| map f (x :: xs) = (f x) :: map f xs;

> val map = fn : (′a −> ′b) −> ′a list −> ′b list

Като пример по-долу, map прилага функцията size върху всеки елемент на

списък от символни низове и резултата е списък съдържащ дължините на

съответните низове.

− map size ["blue", "red", "green"];

> val it = [4, 3, 5] : int list

Page 77: Lectures FP

Въведение в стандарта ML

Функционално програмиране

77

По-нататък, функцията filter прилага предикат към списък. Резултата е

списък от всички елементи удолетворяващи предиката в реда им на срещане.

− fun filter p [] = []

| filter p (x :: xs) = if (p x) then x :: filter p xs

else filter p xs;

> val filter = fn : (′a −> bool) −> ′a list −> ′a list

− filter (fn a => size a = 4) ["blue", "red", "green"];

> val it = ["blue"] : string list

Функционали върху списъци: exists и forall Тези функционали връщат като отговор дали някои или всички елементи

на списъка удолетворяват даден предикат. Те могат да се разглеждат като квантори над елементите на списъка.

− fun exists p [] = false

| exists p (x :: xs) = (p x) orelse exists p xs;

> val exists = fn : (′a −> bool) −> ′a list −> bool

− fun forall p [] = true

| forall p (x :: xs) = (p x) andalso forall p xs;

> val forall = fn : (′a −> bool) −> ′a list −> bool

Горните функционали са частично приложими функции, които превръщат

предикат над тип ′a в предикат над тип ′a list.

− exists (fn a => size a = 4) ["blue", "red", "green"];

> val it = true : bool

− forall (fn a => size a = 4) ["blue", "red", "green"];

> val it = false : bool

Нека сега да дефинираме функцията disjoint, която проверява дали два списъка

са съставени от различни елементи:

− fun disjoint (xs, ys) =

forall (fn x => forall (fn y => x <> y) ys) xs;

> val disjoint = fn : ′′a list * ′′a list −> bool

Page 78: Lectures FP

Въведение в стандарта ML

Функционално програмиране

78

Трудно е да се забележи, но всъщност, disjoint проверява дали за всяко x в xs и

за всяко y в ys е изпълнено x ≠ y.

− disjoint ([1, 2, 3], [3, 4, 5, 6]);

> val it = false : bool

2. Безкрайни списъци и отложени пресмятания Безкрайните списъци, наричани още lazy списъци, са една от най-

забележителните черти на функционалното програмиране. Елементите на lazy

списъците не се изчисляват докато техните стойности не са нужни на

програмата. Така lazy списъците могат да бъдат безкрайни. В lazy езиците като

Miranda всички структури от данни са lazy и безкрайните списъци са често

срещани в програмата. В ML, който не е lazy функционален език, безкрайните

списъци са рядкост. Тук ще покажем как се представят lazy списъците в ML, а

именно ще изразяваме опашката на списъка чрез функция за да забавим

нейното изчисляване. Досега ние очаквахме от всяка функция да даде резултат

в крайно време. Освен това всяка рекурсивна функция включваше базов случай,

при който рекурсията преключваше. Сега ние ще работим с потенциално

безкрайни резултати. Можем да видим крайна част от списъка, но не и целия

списък. Така то е възможно да се съберат два безкрайни списъка елемент по

елемент, но не е възможно да се намери например, най-малкия елемент на

списък. Рекурсивните функции, които ще дефинираме тук няма да имат базов

случай. Ние ще се интересуваме не дали програмата прекъсва, а дали тя

генерира крайна част от своя резултат в крайно време.

Тип последователност Безкрайните (Lazy) списъци в ML се наричат последователности.

Последователността, подобно на списъка, е или празна или съдържа глава и

опашка. Празната последователност е Nil, непразната има формата Cons(x, xf),

където x е главата, а xf е функция за изчисляване на опашката.

Page 79: Lectures FP

Въведение в стандарта ML

Функционално програмиране

79

− datatype ′a SEQ = Nil

| Cons of ′a * (unit −> ′a SEQ);

Функциите които връщат главата и опашката на последователност се лесни за

дефиниране. Функцията, която изчислява опашката на последователност се

прилага върху аргумента (), т.е. единствената стойност на типа unit.

Следователно, аргумента на тази функция не носи информация.

− fun head (Cons(x, _)) = x;

> val head = fn : ′a SEQ −> ′a

− fun tail (Cons(_, xf)) = xf ();

> val tail = fn : ′a SEQ −> ′a SEQ

Нека да дефинираме нарастваща последователност от цели числа

започваща от k.

− fun from k = Cons(k, fn () => from (k+1));

> val from = fn : int −> int SEQ

− from 1;

> val it = Cons(1, fn) : int SEQ

Последователността започва от 1 и сега можем да приложим tail върху нея и да

получим опашката представена като Cons(2, fn).

− tail it;

> val it = Cons(2, fn) : int SEQ

Следващата функция връща първите n елемента на последователност като

списък:

− fun takeq (0, xq) = []

| takeq (n, Nil) = []

| takeq (n, Cons(x, xf)) = x :: takeq (n-1, xf ());

> val takeq = fn : int * ′a SEQ −> ′a list

Нека като пример приложим функцията takeq към последователността from 25.

− takeq (5, from 25);

> val it = [25, 26, 27, 28, 29] : int list

Page 80: Lectures FP

Въведение в стандарта ML

Функционално програмиране

80

Елементарни операции върху последователности За да бъде изчислима една функция върху последователности е

необходимо всяка крайна част от изхода да зависи в най-добрия случай от

крайна част от входа. Нека да разгледаме следващата функция, която повдига

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

− fun squares Nil : int SEQ = Nil

| squares (Cons(x, xf)) = Cons(x*x, fn () => squares (xf ()));

> val squares = fn : int SEQ −> int SEQ

Така когато се изчислява опашката на изхода се прилага squares към опашката

на входа.

− squares (from 1);

> val it = Cons(1, fn) : int SEQ

− takeq (6, it);

> val it = [1, 4, 9, 16, 25, 36] : int list

Функцията, която добавя една последователност към друга работи като тази дефинирана за списъци. Първо се вземат елементите от първата последователност и когато тя стане празна се добавят елементите от втората.

− fun appendq (Nil, yq) = yq

| appendq (Cons(x, xf), yq) = Cons(x, fn () => appendq (xf (), yq));

> val appendq = fn : ′a SEQ * ′a SEQ −> ′a SEQ

Нека сега да дефинираме функционалите map и filter за последователности. Всъщност, функцията squares е пример на map, тъй като прилага функцията за повдигане на квадрат към всеки елемент на последователност.

− fun mapq f Nil = Nil

| mapq f (Cons(x, xf)) = Cons(f x, fn () => mapq f (xf ()));

> val mapq = fn : (′a −> ′b) −> ′a SEQ −> ′b SEQ

Функцията filterq извиква функцията за изчисляване на опашката докато се

намери елемент удолетворяващ дадения предикат. Ако няма такъв елемент

изчислението никога не прекъсва.

Page 81: Lectures FP

Въведение в стандарта ML

Функционално програмиране

81

− fun filterq p Nil = Nil

| filterq p (Cons(x, xf)) = if p x then Cons(x, fn () => filterq p (xf ()))

else filterq p (xf());

> val filterq = fn : (′a −> bool) −> ′a SEQ −> ′a SEQ

− filterq (fn n => n mod 3 = 0) (from 1); > val it = Cons (3, fn) : int SEQ − takeq (5, it); > val it = [3, 6, 9, 12, 15] : int list Последователност от прости числа може да бъде изчислена като се

използва известния алгоритъм решето на Ератосфен. Започва се с последователност [2, 3, 4, 5, 6, ...]; Взема се 2 като просто число. Изтриват се всички числа които се делят на 2, тъй като те не са прости. Така получаваме [3, 5, 7, 9, 11, ...]; Взема се 3 като просто и се изтриват неговите кратни, т.е. последователността се редуцира до [5, 7, 11, 13, 17, ...]; Взема се 5 като просто ....

На всяка стъпка последователността съдържа онези числа, които не се делят на никое от предхождащите прости. Така главата на последователността е винаги просто число и процеса може да продължи безкрайно. Функцията sift използва filterq за да изтрие от последователността числата кратни на разглежданото просто число докато sieve повторно претърсва последователността.

− fun sift pr = filterq (fn n => n mod pr <> 0);

> val sift = fn : int −> int SEQ −> int SEQ

− fun sieve (Cons (pr, nf)) = Cons (pr, fn () => sieve (sift pr (nf())));

> val sieve = fn : int SEQ −> int SEQ

− val primes = sieve (from 2); > val primes = Cons(2, fn) : int SEQ − takeq (10, primes); > val it = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] : int list

Когато пишем подобни програми ML типовете ни помагат да избегнем възможното объркване между последователностите и функциите за изчисляване на опашката. Така последователността има тип ′a SEQ, а функцията за изчисляване на опашката unit −> ′a SEQ.

Page 82: Lectures FP

Въведение в стандарта ML

Функционално програмиране

82

Задачи за упражнение

1. Да се дефинира обобщена функция сортираща списък от елементи чрез вмъкване, която може да се прилага над елементи от произволен тип (в лаб. упр. 6 е дефинирана функция реализираща сортиране чрез вмъкване за списък от реални числа).

2. Да се дефинира функционал, който изчислява сумата ∑ −

=

1

0)(m

iif , където m е

цяло число, а f е функция. 3. Да се дефинира функционал, който изчислява сумата ∑ −

=

1

0

m

i ∑ −

=

1

0),(n

jjig

където m и n са цели числа, а g е функция.

4. Да се дефинира функционал, който пресмятa min1−

=

m

oi f(i) на функцията f,

където m е дадено цяло число. 5. Да се дефинира функционал takewhile, който връща начален сегмент от

списък в които елементите удолетворяват даден предикат, т.е. до срещането на първия елемент, който не го удолетворява.

6. Да се дефинира функционал dropwhile, който връща останалите елементи от списък започвайки от първия, който неудолетворява даден предикат.

7. Да се дефинира функция foldleft, която изпълнява следното: foldleft ⊕ (e, [x1, x2, ..., xn])=(…((e⊕ x1) ⊕ x2) …⊕ xn), където ⊕ е двуаргументна инфиксна функция.

8. Да се дефинира функция foldright, която изпълнява следното: foldright ⊕ ([x1, x2, ..., xn], e)=(x1 ⊕ ( x2 ⊕…( xn ⊕ e)…)), където ⊕ е двуаргументна инфиксна функция.

9. Да се дефинира функция iterates, която генерира следна последователност: [x, f(x), f(f(x)), …, f k(x), …].

10. Да се дефинира функция, която по зададено цяло число k и последователност [x1, x2, ...] връща нова последователност в която всеки елемент се повтаря k пъти.

11. Да се дефинира функция, която трансформира дадена последователност [x1, x2, x3, x4, ...] както следва [x1 + x2, x3+ x4, ...].

Page 83: Lectures FP

Въведение в стандарта ML

Функционално програмиране

83

ЛАБОРАТОРНО УПРАЖНЕНИЕ No 10

АБСТРАКТНИ ТИПОВЕ ДАННИ

При решаването на всеки проблем трябва да се реши как да се представи

информацията относно приложението в програмата. Желателно е вродената

структура на реалните обекти добре да рефлектира в данните обекти, които се

използват. Обаче, някои реални обекти имат свойства, които не могат да бъдат

представени само с конкретни типове данни (datatype декларации) и

съпоставяне на образци. Този проблем може да бъде решен като се използва

конкретен тип данни за представянето и се въведат нови операции с точните

свойства, които да работят над това представяне. Чрез дефинирането на

абстрактен тип данни е възможно да се скрият свойствата на представянето и да

се показват само тези от тях, които подхождат на разглеждането приложение.

1. Абстракция на данни Ако може да се ограничи зависимостта на програмата от конкретно

представяне, то тя може да се променя по-лесно. За да се скрият детайлите на

представянето и да се направят видими само важните свойства трябва да се

направят абстрактни. Такива абстракции се нарича абстракция на данни. Има

две причини за абстрактното представяне на данните: да се ограничи

зависимостта от конкретно представяне и нуждата да се моделират реални

понятия с неправилни свойства.

Нека като пример да разгледаме как ще бъде дефинирана опашка с

абстрактен тип данни. Тя може да бъде представена чрез списък. За да

дефинираме абстрактен тип ние се нуждаем от конкретен тип данни за

представянето, за което използваме конструктора Queue.

− exception Remove and Rest;

Page 84: Lectures FP

Въведение в стандарта ML

Функционално програмиране

84

− abstype 'a QUEUE = Queue of 'a list

with val Empty = Queue nil

fun Enter e (Queue es) = Queue (e::es)

fun remove (Queue nil) = raise Remove

| remove (Queue [e]) = e

| remove (Queue (e :: es)) = remove (Queue es)

fun rest (Queue nil) = raise Rest

| rest (Queue [e]) = Queue nil

| rest (Queue (e :: es)) = Enter e (rest (Queue es))

fun isempty (Queue nil) = true

| isempty (Queue _) = false

end;

Тази дефиниция въвежда нов абстрактен тип данни QUEUE с пет операции:

конструктори Empty и Enter, селектори remove и rest, и предикат isempty.

Обектите от типа QUEUE са представени чрез друг тип, който е конкретен тип

следващ след знака за равенство, а именно Oueue of 'a list.

Представянето на обектите е скрито в смисъл, че програма съдържаща

горната дефиниция може да манипулира с обектите само чрез петте операции.

Конструктура Queue на типа, понякога наричан конкретен конструктор, не е

видим извън дефиницията. Визуално може да си представим, че абстрактните

конструкторите Empty и Enter построяват представяне на обект и го поставят в

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

и да се види нейното съдържание.

− Enter 7 Empty; > val it = <QUEUE> : int QUEUE

Когато обаче, се приложи един от селекторите към черната кутия тогава представянето се взема от черната кутия и се съпоставя срещу образеца.

− remove it; > val it = 7 : int

Page 85: Lectures FP

Въведение в стандарта ML

Функционално програмиране

85

За конкретните типове операциите за равенство се дефинират автоматично, но те не са винаги точно тези от които се нуждаем. Да предположим, че множество от цели числа е представено чрез списък от цели числа без повторения.

− datatype SET = Set of int list;

> datatype SET = (SET,{con Set : int list −> SET})

− Set [1, 2] = Set [2, 1];

> val it = false : bool

Очевидно горната операция проверява дали два обекта са конструирани по един и същи начин, а не дали те представят едно и също множество. За да разрешим този проблем ще дефинираме абстрактен тип данни. За конструктори ще използваме Empty и Insert.

− abstype SET = Set of int list

with val Empty = Set nil

fun Insert e (Set es) =

if e mem es then Set es else Set (e :: es)

fun member e (Set es) = e mem es

fun eqset (Set es) (Set fs) = sort es = sort fs

end;

Така като се използва eqset ще получим, че [1, 2] и [2, 1] представят едно и също множество. Допускаме, че sort е функция за сортиране на списъци, която е дефинирана предварително.

2. Последователни файлове Файловете се използват в повечето операционни системи и са валидни в

повечето езици за програмиране. Файла прилича на опашката, тъй като елементите се въвеждат в края на файла. При четене от файл елементите се сканират подред и има указател към текущата позиция. Отново ще използваме конструктурите Empty и Insert. За да пазим текущата позиция във файла ще въведем трети конструктор Cursor. Например, да разгледаме файл, съдържащ числата 3, 7 и 9, и нека указателят му да сочи втория елемент. Този файл ще бъде представен чрез Insert(9, Cursor(Insert(7, Insert(3, Empty)))).

Абстрактната операция Rewrite създава празен файл, reset поставя указателя преди първия елемент на файла, put добавя елемент в края на файла, get връща текущия елемент, advance премества указателя на следващия елемент

Page 86: Lectures FP

Въведение в стандарта ML

Функционално програмиране

86

и eof проверява дали е достигнат края на файла. Отбелязваме, че reset и advance се разглеждат като конструктори, тъй като те се използват да получим файла когато указателя не е в края на файла. При изпълнението на по-горе описаната структура се сблъскваме с един проблем. Необходимо е да се дефинира помощна функция, която да изтрива указателя от файла когато се изпълнява операцията reset, но тази функция не трябва да бъде видима извън дефиницията. За това тя ще бъде дефинирана чрез локална декларация.

− abstype 'a FILE = Empty | Insert of 'a * 'a FILE | Cursor of 'a FILE

with exception Put and Advance and Get and Reset

val Rewrite = Cursor Empty

fun put x (Cursor Empty) = Insert(x, Cursor Empty)

| put x (Insert(y, f)) = Insert(y, put x f)

| put _ _ = raise Put

fun advance (Cursor(Insert(x, f))) = Insert(x, Cursor f)

| advance (Insert(x, f)) = Insert(x, advance f)

| advance _ = raise Advance

local fun remove (Cursor f) = f

| remove (Insert(x, f)) = Insert(x, remove f)

| remove _ = raise Reset

in fun reset (Cursor f) = Cursor f

| reset (Insert(x, f)) = Cursor(Insert(x, remove f))

| reset _ = raise Reset

end

fun get (Cursor(Insert(x, _))) = x

| get (Insert(_, f)) = get f

| get _ = raise Get

fun eof (Cursor Empty) = true

| eof (Cursor _ ) = false

| eof (Insert( _, f)) = eof f

end;

Page 87: Lectures FP

Въведение в стандарта ML

Функционално програмиране

87

Друг по-ефективен начин за представянето на файл е чрез два списъка. Първият списък ще съдържа сканираните елементи, а вторият елементите, които трябва да бъдат сканирани. При преместването на указателя ще се премества първия елемент от втория списък в началото на първия списък. Нека да видим как ще изглежда този абстрактен тип данни.

− abstype 'a FILE = File of 'a list * 'a list

with exception Put and Advance and Get

val Rewrite = File (nil, nil)

fun put x (File(xs, nil)) = File(x :: xs, nil)

| put x _ = raise Put

fun advance (File( _, nil)) = raise Advance

| advance (File(xs, y :: ys)) = File(y :: xs, ys)

fun reset (File(xs, ys)) = File(nil, rev xs @ ys)

fun get (File( _ , nil)) = raise Get

| get (File( _ , y :: ys)) = y

fun eof (File( _ , ys)) = null ys

end;

3. Множества Множеството е съвкупност от обекти, в която броя на появяването на

обект е от значение. Основната операция върху множество е проверка дали

обект е елемент на множеството. Други операции са обединение и сечение на

множества.

За нуждите на следващата абстрактна дефиниция ще предефинираме

функцията mem, дадена в лаб. упр. 5, като функция която връща друга функция

като резултат, т.е. частично приложима функция.

− fun memb [] x = false

| memb (y :: ys) x = (x = y) orelse (memb ys x);

> val memb = fn : ''a list −> ''a −> bool

После множествата ще бъдат представени като списъци без повторения.

Page 88: Lectures FP

Въведение в стандарта ML

Функционално програмиране

88

− abstype 'a SET = Set of 'a list

with val Empty = Set nil

fun Insert e (Set es) =

if memb es e then Set es else Set (e :: es)

fun member (Set es) e = memb es e

fun union (Set es) (Set fs) = Set (es @ filter (not o memb es) fs)

fun intersect (Set es) (Set fs) = Set(filter (memb fs) es)

end;

Отбелязваме, че при дефинирането на функцията union е използван инфиксния

оператора o за композиция на функции в ML, т.е. прилагане на функция върху

резултата от друга функция. Той е дефиниран чрез:

− infix o;

− fun (f o g) x = f (g x);

> val o = fn : ('b −> 'c) * ('a −> 'b) −> 'a −> 'c

В горната абстрактна дефиниция са включени най-основните операции

върху множеста. Тя може да бъде разширена и с други операции. Например,

може да се дефинира функция cardinality, която връща броя на елементите в

крайно множество.

− fun cardinality (Set es) = length es;

> val cardinality = fn : 'a SET −> int

Отбелязваме, че крайните множества могат да бъдат тествани за равенство ако техните елементи могат. Така, следващата функция проверява дали две множества са равни като проверява дали са с еднаква мощност и освен това дали всеки елемент от първото множество е елемент на второто.

− fun eqset (Set es) (Set fs) =

let fun conjl nil = true

| conjl (x :: xs) = x andalso conjl xs

in length es = length fs andalso conjl (map (memb fs) es)

end;

> val eqset = fn : 'a SET −> 'a SET −> bool

Page 89: Lectures FP

Въведение в стандарта ML

Функционално програмиране

89

Задачи за упражнение 1. Операцията back премества указателя на файл една стъпка назад.

Допълнете с тази операция двете дадени представяния на файлове. 2. При файлове с директен достъп е възможно да се чете и пише във всяка

позиция на файла. Освен това е възможно указателя да се премества на произволна позиция във файла чрез задаване на номера на елемента на който искаме да отидем. За да се записва в края на файла е необходимо първо указателя да се премести на позицията непосредствено след файла. Дефинирайте абстрактен тип данни реализиращ тази структура.

3. Операцията difference реализира разликата на две множества. Допълнете с тази операция даденото представяне на множество.

4. Операцията subset проверява дали едно множество се съдържа в друго. Допълнете с тази операция даденото представяне на множество.

5. Да се дефинира функцията makeset, която създава множество от всички елементи на списък, т.е. списък без повторения.

6. Да се дефинира операцията setfilter, която избира онези елементи от множество, които удолетворяват дадено условие.

7. Да се дефинира операцията setmap, която прилага функция към всеки елемент на множество.

8. Да се дефинира функция, която формира множество от наредени двойки от елементи на две дадени множества, които удолетворяват дадено условие, т.е. )}.,(21|),{( yxPSySxyx ∧∈∧∈

9. Да се дефинира абстрактен тип таблица като списък от наредени двойки.

Page 90: Lectures FP

Въведение в стандарта ML

Функционално програмиране

90

Page 91: Lectures FP

Въведение в стандарта ML

Функционално програмиране

91

ЛИТЕРАТУРА

1. A. Wikstrom, Functional Programming Using Standard ML, Prentice-Hall, 1987.

2. L. Pauson, ML for the Working Programmer, Cambridge University Press, 1992.

3. R. Milner, M. Tofte and R. Harper, The Definition of Standard ML, The MIT Press, 1990.

4. J.D. Ulman, Elements of ML Programming, Prentice-Hall, 1993. 5. C. Read, Elements of Functional Programming, Addison-Wesley, 1989. 6. R. Bird and P. Wadler, Introduction to Functional Programming, Prentice-

Hall, 1988. 7. C. Myers, C. Clack and E. Poon, Programming in Standard ML, Prentice-Hall,

1993. 8. R. Harper, Introduction to Standard ML, LFCS Report Series, Department of

Computer Science, University of Edinburgh, 1989.