Садржај

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

Поред имплементације алгоритама, програмирање подразумева и дефинисање структура података. У језику Haskell постоји неколико начина да дефинишемо нове типове података. Ми ћемо се фокусирати на алгебарске типове података, који се дефинишу помоћу кључне речи data.

Најједноставнија употреба алгебарских типова је дефинисање набројивих типова. На пример,

data Color = Red | Green | Blue

Овим смо дефинисали тип Color и три константе (Red, Green и Blue) које су овог типа.

Алгебарске типове можемо користити и за дефинисање структурних типова података (по узору на struct у језику C#). На пример,

data Person = Person { name::String, yearOfBirth::Int }

Овим смо дефинисали тип Person. Свака особа има име и годину рођења. Уз тип смо добили и функцију (тзв. конструктор) Person, која прави податак типа Person од ниске и броја. На пример,

Person "Petar Petrović" 2007

је податак типа Person (немојте се збунити тиме што смо име типа и име конструктора означили исто – на то сте навикли и у објектно–оријентисаним језицима). Имена поља name и yearOfBirth се могу користити за јаснију дефиницију података типа Person.

Person {name="Petar Petrović", yearOfBirth=2007}

Имена поља су уједно функције које читају одговарајућа поља. На пример, name је функција типа name :: Person -> String.

Prelude> name (Person "Petar Petrović" 2007)
"Petar Petrović"

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

data Person = Person String Int

Тип може имати и више конструктора. На пример, особа је или запослена или је студент.

data Person =
   Employee {name::String, salary::Double}
 | Student {name::String, int::year}

Дефинисање функција над алгебарским типовима је обично засновано на уклапању шаблона по разним конструкторима. На пример:

hello :: Person -> String
hello (Employee name salary) = "Hello, my name is " ++ name
hello (Student name year) = "Hello, I am " ++ show year ++ ". year student"

Приметимо да смо за конверзију године у ниску карактера морали употребити функцију show. Ова функција је дефинисана за различите типове података (све типове који припадају класи типова Show) и служи да се они могу приказати тј. да се податак тог типа претвори у ниску карактера којом се представља.

Уклапање шаблона може да се врши и у склопу израза case-of. На пример:

personType :: Person -> String
personType person = case person of
                      Employee _ _ -> "employee"
                      Student _ year -> show year ++ ". year student"

Наравно, исто је могло бити урађено на једноставнији начин:

personType :: Person -> String
personType (Employee _ _) = "employee"
personType (Student _ year) -> show year ++ ". year student"

Пуна снага алгебарских типова података види се у случају рекурзивних (каже се и индуктивних) типова података. Листа је основни пример таквог типа података. Листа је или празна или се добија надовезивањем елемента на почетак неке листе (репа). Тип листе целих бројева би се могао дефинисати на следећи начин:

data List =
  Empty
| Cons Int List

Листа је или празна (конструктор Empty) или се добија надовезивањем броја на листу (конструктор Cons). Пример податка овог типа је

Cons 1 (Cons 2 (Cons 3 Empty))

којим се суштински представља листа [1, 2, 3]. Функције које обрађују индуктивне типове података обично су рекурзивне.

sum :: List -> Int
sum Empty = 0
sum (Cons x xs) = x + sum xs

Примећујете велику сличност са рекурзивним функцијама за обраду уграђених листа (једина разлика је то што се тамо празна листа обележава са [] уместо са Empty, а надовезивање са x:xs уместо са Cons x xs).

У наставку ћемо приказати како је коришћењем индуктивних алгебарских типова података могуће дефинисати неколико веома корисних структура података.

Бинарно дрво

Циљ нам је да направимо структуру података којом можемо да представљамо скупове елемената тако да ефикасно можемо да убацујемо нове елементе и да проверавамо да ли елементи припадају скупу. Једна погодна структура података за представљање скупа је бинарно дрво. Бинарно дрво је рекурзивна структура података која се састоји од чворова. У чвору је уписана нека вредност (претпоставићемо, једноставности ради, да је у питању вредност типа Int), а лево и десно се налазе мања дрвета. Излаз из ове рекурзије представља празно дрво које нема ни вредност ни наследнике. Тип података за представљање дрвета може бити дефинисан на следећи начин:

data Tree =
   Empty |
   Node Tree Int Tree

Дрво је, дакле, или празно (Empty) или је у питању чвор који садржи три податка: лево поддрво, вредност типа int и десно поддрво.

Сада можемо да формирамо изразе чији је тип Tree. На пример

Node (Node Empty 2 Empty) 3 (Node Empty 5 (Node Empty 7 Empty))
../_images/tree.png

Нажалост, приликом њиховог уноса у интерпретатор добићемо поруку да систем не уме да прикаже дрво. Најједноставнији начин да се то реши је да се аутоматски генерише функција за приказ дрвета, што се постиже тиме што се након дефиниције типа дода deriving Show.

data Tree =
   Empty |
   Node Tree Int Tree
   deriving Show

На овај начин је аутоматски дефинисана функција show :: Tree -> String која на основу датог дрвета гради ниску карактера која представља дрво у истом формату у ком се оно може задати у програму (нпр. Node Empty 2 Empty).

Show је заправо класа типова која садржи типове за које постоји функција show која може да претвори податак тог типа у ниску. Клаузулом deriving Show поред аутоматског генерисања функције show се истовремено наглашава да ће дефинисани тип података припадати класи show. Ми се нећемо детаљније бавити класама типова. Покушај на интернету да пронађеш више информација о овој теми.

Приказани запис дрвета је релативно компликован. Једноставније би било да, на пример, претходно дрво представимо ниском (.2.)3(.5(.7.)) и да дефинишемо функцију parseTree која од такве ниске гради дрво. Употребићемо технику рекурзивног спуста, коју сте срели у првом разреду. Користимо следећу контекстно-слободну граматику.

\[\begin{split}\begin{eqnarray*} \mathit{drvo} &\rightarrow& .\\ \mathit{drvo} &\rightarrow& ( drvo\ broj\ drvo ) \end{eqnarray*}\end{split}\]

Пошто су изрази потпуно заграђени, на основу првог карактера одређујемо да ли се примењује прво или друго правило. У другом правилу прво треба да прескочимо отворену заграду, затим рекурзивно да прочитамо лево поддрво, затим да прочитамо број, па да рекурзивно прочитамо десно поддрво и да на крају прочитамо десну заграду. Сваки позив функције парсирања треба да прочита садржај са почетка и да врати прочитано дрво, али и преостали део ниске, иза прочитаног садржаја. Стога дефинишемо помоћну функцију parseTree' која то ради, док главна функција враћа само дрво, занемаривши преостали садржај ниске (ако је ниска исправна, преостали садржај ће бити празан). Наредна имплементација претпоставља да је ниска која се парсира увек исправно задата.

import Data.Char (isDigit)

parseTree :: String -> Tree
parseTree str = fst (parseTree' str)
  where
    parseTree' :: String -> (Tree, String)
    parseTree' str =
      if head str == '.' then
        (Empty, tail str)
      else
        let str1 = tail str;                       -- skip (
            (left, str2) = parseTree' str1         -- read left subtree
            (valueStr, str3) = span isDigit str2   -- read value
            (right, str4) = parseTree' str3        -- read right subtree
            str5 = tail str4                       -- skip )
        in (Node left (read valueStr) right, str5)

Обратимо пажњу на примену функције span isDigit. Функција isDigit :: Char -> Bool је увезена из библиотеке Data.Char (зато је на почетку програма морала бити наведена декларација import) и проверава да ли је дати карактер цифра. Функција span :: (a -> Bool) -> [a] -> ([a], [a]) дели листу на два дела: елементе са почетка листе који задовољавају услов дат као аргумент функције span и остатак листе. Функција read чита бројевну вредност из ниске која представља исправан запис броја.

За представљање скупа ми ћемо користити уређена тј. претраживачка дрвета (енг. binary search tree). Њихова основна особина је да сваки чвор задовољава да се у левом поддрвету налазе вредности које су мање или једнаке од вредности у том чвору, а у десном поддрвету вредности које су веће или једнаке од вредности у том чвору. Ако не желимо дупликате (а скуп обично не садржи дупликате), онда захтевамо да важе стриктне неједнакости (лево су строго мањи, а десно строго већи елементи од оног уписаног у чвору). На основу овога можемо дефинисати рекурзивну функцију за уметање елемента у дрво. Она прима дрво и вредност која се умеће и враћа ново дрво. Приметимо да се већи део полазног дрвета задржава, тако да овај приступ није меморијски превише захтеван (пошто се у функционалном програмирању обично не врши модификација података, они могу бити дељени, без копирања).

Уметање вредности у право дрво даје дрво коме је та вредност у корену, а лево и десно су празна поддрвета. Уметање у непразно дрво подразумева поређење вредности x, која се умеће, са вредношћу уписаном у корен дрвета. Ако је x мање од корена, врши се рекурзивно убацивање у лево поддрво (вредност у корену и десно поддрво остају неизмењени); ако је веће врши се рекурзивно убацивање у десно поддрво (вредност у корену и лево поддрво остају неизмењени), а ако је једнако вредности у корену, онда се цело дрво враћа неизмењено (не желимо да убацујемо дупликате).

insert :: Tree -> Int -> Tree
insert Empty x = Node Empty x Empty
insert (Node left root right) x
   | x < root  =  Node (insert left x) root right
   | x > root  =  Node left root (insert right x)
   | otherwise =  Node left root right

Понављањем ове функције лако можемо да формирамо дрво од листе бројева. Тај поступак можемо имплементирати кроз функцију која формира дрво од елемената листе.

treeOfList :: [Int] -> Tree
treeOfList = foldl insert Empty

Сложеност функције insert и treeOfList зависи од распореда чворова дрвета. У најгорем случају, када је листа од које се формира дрво сортирана, сви чворови ће завршити на једној страни (увек ће лево или ће увек десно поддрво бити празно, у зависности од тога како је листа сортирана). Тада је сложеност функције insert линеарна, а функције treeOfList квадратна. Ипак, ако су елементи који се убацују у дрво насумични, можемо очекивати да ће дрво бити прилично балансирано (да ће број чворова са леве и са десне стране сваког чвора бити отприлике једнак) и тада ће висина дрвета логаритамски зависити од броја чворова дрвета, па ће сложеност функције insert бити \(O(\log n)\), а сложеност функције treeOfList бити \(O(n \log n)\).

Уместо обичних претраживачких дрвета могуће је користити и тзв. самобалансирајућа дрвета. Најпознатије врсте таквих дрвета су АВЛ дрвета и Црвено-црна дрвета. Алгоритам уметања у ова дрвета је такав да се приликом уметања проверава да ли је нарушена равнотежа између левог и десног дела и ако јесте, дрво се ротира тако да би се та равнотежа поново успоставила.

Када је формирано дрво, тада можемо вршити његову претрагу, тј. можемо имплементирати функцију која проверава да ли дрво садржи дати елемент. Празно дрво не садржи ниједан елемент. Непразно дрво претражујемо тако што елемент који тражимо поредимо са вредношћу у корену. Ако је вредност мања од корена, тражимо је рекурзивно у левом поддрвету, ако је већа, у десном, а ако је једнака вредности у корену, онда знамо да дрво садржи тражену вредност.

contains :: Tree -> Int -> Bool
contains Empty x = False
contains (Node left root right) x
  | x < root    = contains left x
  | x > root    = contains right x
  | otherwise   = True

Ово је вероватно добар тренутак да упоредимо приказано решење у језику Haskell са решењем у традиционалном императивном програмском језику, какав је C.

#include <stdio.h>
#include <stdlib.h>

struct Node {
   int value;
   struct Node *left, *right;
};

Node* newNode(int value) {
      Node* new = (Node*)malloc(sizeof(Node));
      new->value = value;
      new->left = new->rigth = NULL;
}

void freeNode(Node* node) {
   if (node != NULL) {
      freeNode(node->left);
      freeNode(node->right);
      free(node);
   }
}

Node* insert(Node* root, int value) {
   if (root == NULL)
      return newNode(value);
   if (value < root->value)
      root->left == insert(root->left, value);
   else if (value > root->value)
      root->right = insert(root->right, value);
   return root;
}

int contains(Node* root, int value) {
   if (root == NULL)
      return 0;
   if (value < root->value)
      return contains(root->left, value);
   else if (value > root->value)
      return contains(root->right, value);
   else
      return 1;
}

Node* treeOfArray(int[] a, int n) {
    Node* root = NULL;
    for (int i = 0; i < n; i++)
       root = insert(root, a[i]);
    return root;
}

void printTree(Node* root) {
   if (root != NULL) {
       printTree(root->left)
       printf("%d ", root->value);
       printTree(root->right);
   }
}

int main() {
    int a = {1, 3, 2, 5, 4};
    int n = sizeof(a) / sizeof(int);
    Node* root = treeOfArray(a, n);
    printTree(root);
    freeNode(root);
}

Примећујемо да су алгоритми који се користе исти као у одговарајућим Hakell функцијама, али да је кôд оптерећен многим техничким детаљима о којима није било потребе водити рачуна у Haskell имплементацији. На пример, у језику C је било потребно ручно алоцирати и ослобађати меморију (коришћењем библиотечких функција malloc и free). Употреба показивача је експлицитна, што на многим местима чини да је кôд оптерећен додатним симболима (попут *, ->) и програм је теже и писати и разумети. Захваљујући библиотечкој функцији fold, имплементација креирања дрвета од листе је много краћа и једноставнија од имплементације у C-у која мора да користи петљу и да ручно имплементира функционалност коју нам пружа fold.

У уређеном дрвету је лако пронаћи минимални и максимални елемент (они се налазе у крајњем левом и крајњем десном чвору).

minTree :: Tree -> Int
minTree (Node Empty x _) = x
minTree (Node left x _ ) = minTree left

Дефинишимо и функцију која уклања тај најмањи елемент дрвета (под претпоставком да је дрво непразно). Функција враћа уређени пар, који се састоји од уклоњене вредности и измењеног дрвета. Ако не постоји лево поддрво корен дрвета је најмањи елемент, а његовим брисањем остаје само десно поддрво. У супротном бришемо најмањи елемент левог поддрвета.

deleteMin :: Tree -> (Int, Tree)
deleteMin (Node Empty x right) = (x, right)
deleteMin (Node left x right)  = let (m, left') = deleteMin left
                                  in (m, Node left' x right)

Ова се функција може искористити и за уклањање дате вредности из скупа (тј. дрвета). Брисањем било чега из празног дрвета дрво остаје празно. Ако је дрво непразно, а вредност која се брише мања од корена, рекурзивно се та вредност брише из левог поддрвета. Ако је вредност већа од корена, рекурзивно се та вредност брише из десног поддрвета. Најкомпликованији је случај када је вредност која се брише једнака вредности у корену. Ако је тада десно поддрво празно, можемо само вратити лево поддрво. У супротном можемо обрисати најмању вредност из десног поддрвета и њу ставити на место корена (аналогно бисмо могли обрисати највећу вредност из левог поддрвета и њу ставити на место корена). Да бисмо анализирали да ли је десно поддрво празно или непразно, можемо употребити израз case-of.

delete :: Int -> Tree -> Tree
delete _ Empty = Empty
delete x' (Node left x right)
   | x' < x      = Node (delete x' left) x right
   | x' > x      = Node left x (delete x' right)
   | otherwise   = case r of Empty -> left
                             _     -> let (m, right') = deleteMin right
                                       in Node left m right'

Од елемената дрвета можемо формирати листу. Ако прво покупимо елементе из левог поддрвета, затим корен, па затим елементе из десног поддрвета, захваљујући уређености дрвета добијена листа ће бити сортирана. На тај начин добијамо још један алгоритам сортирања (енг. tree sort).

listOfTree :: Tree -> [Int]
listOfTree Empty = []
listOfTree (Node left root right) =
   listOfTree left ++ [root] ++ listOfTree right

treeSort :: [Int] -> [Int]
   treeSort = listOfTree . treeOfList

У дрвету које смо дефинисали налазе се увек подаци типа Int, међутим, јасно је да би се на исти начин могли чувати и подаци других типова. Могуће је дефинисати и дрво којем је тип података записаних у чвору параметар (навикли сте на овакве „генеричке“ типове у језику C# где сте користили типове попут List<int> где је тип елемената колекције задат као параметарски тип те колекције).

data Tree a =
   Empty
 | Node (Tree a) a (Tree a)
deriving Show

У овом случају тип означен са a је параметар типа дрво. Дрво садржи лево и десно поддрво (која такође имају елементе типа a) и вредност типа a.

Да бисмо могли формирати претраживачко дрво, тип a мора да подржи поређење елемената, тј. мора бити класе Ord.

insert :: Ord a => Tree a -> a -> Tree a
insert Empty x = Node Empty x Empty
insert (Node left root right) x
   | x < root  =  Node (insert left x) root right
   | x > root  =  Node left root (insert right x)
   | otherwise =  Node left root right

Ред

Листа једноставно може да се користи као стек. Елементи могу да се додају са почетка и скидају са почетка листе у линеарној сложености. Међутим, ред је мало компликованије имплементирати (наравно, постоје библиотечке имплементације, али ћемо га овде, вежбе ради, имплементирати ручно).

Додавање елемената на крај листе има линеарну сложеност, па ред није добро имплементирати помоћу листе. Уобичајени начин имплементације реда у функционалним језицима је тзв. амортизовани ред (енг. amortized queue) који се састоји од две листе.

data Queue a = Queue [a] [a]

У празном реду су обе листе празне.

emptyQueue :: Queue a
emptyQueue = Queue [] []

Елементи се увек додају на почетак прве листе, а скидају са почетка друге.

Операцију додавања елемента у ред је тривијално имплементирати.

push :: a -> Queue a -> Queue a
push x (Queue left right) = Queue (x : left) right

Уклањање елемента из реда је мало компликованије. Функцију ћемо дефинисати тако да врати уклоњени елемент и нови ред. Пошто уклањање из празног реда није могуће, потребно је да се некако у повратној вредности укључи и могућност да је враћена вредност са почетка реда, али и да уклањање није успело. Програмски језици углавном ово решавају тако што типове проширују специјалном вредношћу null која означава да операција није успешно извршена. У функционалним језицима се обично користи посебан тип Maybe који омоћава разликовање исправних и недостајућих вредности.

Тип Maybe a омогућава представљање опционих вредности типа a. Конструктор Nothing означава недостајућу вредност, а Just нормалну вредност. На пример, Just 3 је податак типа Maybe Int. Овај тип се често користи да означи повратне вредности функција које из неког разлога некада не могу да врате исправну вредност. На пример, ако функција успе и треба да врати вредност 3, она ће вратити вредност Just 3, а ако не успе, вратиће вредност Nothing (ово одговара вредности null у програмском језику C#, која служи да означи недостатак исправне вредности).

Предност коришћења посебног у односу на специјалну вредност null је то што је једини начин да се до исправне вредности дође тај да се експлицитно испита да ли је вредност исправна. Ово се обично имплементира или поклапањем шаблона или изразом case.

g x = case f x of
         Nothing -> -1
         Just value -> value

Пошто није могуће скидање вредности из празног реда, Уместо да функција pop враћа пар типа (a, Queue a), она ће враћати пар типа (Maybe a, Queue a).

Aко је цео ред празан, наша функција pop враћа Nothing и празан ред. Ако десна листа није празна, враћамо њен први елемент (упакован у конструктор Just), док ред мењамо тако да му лева листа остаје неизмењена, а десна скраћена за њен почетни елемент. Ако је само десна листа празна, тада обрћемо леву листу и пребацујемо је на десну страну и затим уклањамо први елемент тако добијене листе.

pop :: Queue a -> (Maybe a, Queue a)
pop (Queue [] []) = (Nothing, Queue [] [])
pop (Queue left (x:xs)) = (Just x, Queue left xs)
pop (Queue left []) = let x:xs = reverse left
                       in (Just x, Queue [] xs)

Сложеност најгорег случаја операције pop је линеарна, јер је функција reverse линеарне сложености. Ипак, након једног пребацивања елемената са леве на десну страну, много наредних операција pop ће бити могуће извршити веома брзо. Ако анализирамо пут сваког елемента кроз ред, видимо да ће он једном бити додат на почетак леве листе, једном бити склоњен са почетка леве и додат на почетак десне листе (ово се догађа током извршавања функције reverse) и на крају једном скинут са десне листе. Дакле, сваки елемент учествује у тачно четири операције константне сложености, па се \(n\) елемената реда може обрадити у сложености \(O(n)\), што значи да операције додавања и уклањања елемената имају константну амортизовану сложеност.

Да бисмо могли да експериментишемо са овом структуром података, пожељно је да можемо да је испишемо. Аутоматски генерисана функција исписа (коју можемо добити ако након дефиниције типа ставимо deriving Show) приказује интерну репрезентацију података тј. садржај обе листе.

Ако желимо да прикажемо садржај елемената реда у једној листи (дакле, онако како га замишљамо, а не како је имплементиран), можемо дефинисати своју функцију приказа у облику ниске карактера. То можемо урадити на следећи начин.

show :: Queue a -> String
show (Queue left right) = show (right ++ reverse left)

Ниску добијамо тако што спојимо десну и обрнуту леву листу (тиме је елемент који је први на реду за скидање приказан на почетку). Приметите да ту листу претварамо у ниску позивом функције show, међутим, ово није рекурзивни позив јер је позвана функција show другог типа (она претвара листу, а не ред у ниску карактера).

Претходна функција show се користи када је корисник експлицитно позове, али неће бити позивана аутоматски када год је потребно да се неки ред прикаже на екрану. Да би се то постигло, морамо да користимо механизам класа типова тј. да нагласимо да наш тип Queue припада класи типова Show, при чему се за приказ не користи аутоматски генерисана функција (добијена клаузулом deriving Show), већ специфична функција коју ми дефинишемо. Да би се то урадило, користи се клаузула instance.

instance Show a => Show (Queue a) where
   show :: Queue a -> String
   show (Queue left right) = show (right ++ reverse left)

Овим смо рекли да тип Queue a припада класи типова Show тј. може да се прикаже (део Show (Queue a)), међутим, то важи само под условом да тип а припада класи Show тј. може да се прикаже (део Show a =>). Затим дефинишемо функцију show која дати ред представља у облику ниске карактера.

Вежба – алгебарски типови података

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

Дефинисати тип података за представљање речника (мапе) елемената типа k у елементе типа v. Речник треба да буде реализован помоћу уређеног бинарног дрвета.

data Map k v =  Null
              | Node k v (Map k v) (Map k v)
              deriving Show

Дефинисати функцију која проверава да ли је речник празан.

empty :: Map k v -> Bool
empty Null = True
empty _    = False

Дефинисати функције за уметање тј. ажурирање вредности у речнику.

  • Дефинисати прво функцију insertWithKey која датом кључу придружује дату вредност ако тај кључ већ не постоји у речнику, а ако постоји онда му придружује нову вредност применом задате функције на тај кључ, стару вредност и нову вредност.

  • Коришћењем претходне функције дефинисати затим функцију insert, која датом кључу придружује дату вредност (ако кључ већ постоји у речнику, стара вредност се занемарује).

insertWithKey :: Ord k => (k -> v -> v -> v) -> k -> v -> Map k v -> Map k v
insertWithKey f k v Null = Node k v Null Null
insertWithKey f k v (Node k' v' l r)
  | k < k'   = Node k' v' (insertWithKey f k v l) r
  | k > k'   = Node k' v' l (insertWithKey f k v r)
  | k == k'  = Node k' (f k v' v) l r

insert :: Ord k => k -> v -> Map k v -> Map k v
insert = insertWith (\ v v' -> v')

Дефинисати функцију која листу парова (кључ, вредност) претвара у речник.

fromList :: Ord k => [(k, v)] -> Map k v
fromList = foldl (\m (k, v) -> insert k v m) Null

Дефинисати функцију find, која проналази вредност придружену датом кључу у речнику. Резултат треба да буде типа Maybe v, што значи да треба да буде Nothing ако кључ не постоји у речнику, односно Just v ako je кључу придружена вредност v.

Коришћењем функције find дефинисати функцију findWithDefault, која враћа вредност придружену датом кључу или задату подразумевану вредност ако кључ не постоји у речнику.

Коришћењем функције find дефинисати функцију containsKey, која за дату мапу враћа функцију која проверава да ли кључ постоји у речнику.

find :: Ord k => Map k v -> k -> Maybe v
find Null _ = Nothing
find (Node k v l r) k'
  | k' < k  = find l k'
  | k' > k  = find r k'
  | k' == k = Just v

findWithDefault :: Ord k => v -> k -> Map k v -> v
findWithDefault dflt k m = case find m k of Nothing -> dflt
                                            Just v -> v

containsKey :: Ord k => Map k v -> (k -> Bool)
containsKey m k = case search m k of Nothing -> False
                                     Just _ -> True

Дефинисати функцију adjustWithKey која мења вредност датом кључу тако што нову вредност одређује применом дате функције на кључ и стару вредност.

adjustWithKey :: Ord k => (k -> v -> v) -> k -> Map k v -> Map k v
adjustWithKey f k Null = Null
adjustWithKey f k (Node k' v' l r)
  | k < k'   = Node k' v' (adjustWithKey f k l) r
  | k > k'   = Node k' v' l (adjustWithKey f k r)
  | k == k'  = Node k' (f k v') l r

Дефинисати функцију која уклања најмањи кључ из речника.

Дефинисати затим функцију која уклања дати кључ из речника.

deleteMin :: Map k v -> ((k, v), Map k v)
deleteMin (Node k v Null r) = ((k, v), r)
deleteMin (Node k v l r) = let ((k', v'), d) = deleteMin l
                            in ((k', v'), Node k v d r)

delete :: Ord k => k -> Map k v -> Map k v
delete k Null = Null
delete k (Node k' v' l r)
 | k < k'  = Node k' v' (delete k l) r
 | k > k'  = Node k' v' l (delete k r)
 | k == k' = case r of Null -> l
                       _    -> let ((k', v'), d) = deleteMin r
                                in Node k' v' l d

Дефинисати функционал fold за речник. Он као први аргумент прима функцију која на основу претходног резултата, кључа и вредности придружене кључу ажурира резултат, као други аргумент прима почетну вредност резултата, а као трећи аргумент прима речник.

Коришћењем тако дефинисаног функционала fold дефинисати функцију size, која израчунава величину речника (број кључева у речнику), затим функцију toList, која креира листу парова (кључ, вредност) из речника, сортирану по кључевима и на крају функцију union која прави унију два речника (ако кључ постоји у оба речника потребно је придружити му вредност из другог).

foldMap :: (a -> k -> v -> a) -> a -> Map k v -> a
foldMap _ i Null = i
foldMap f i (Node k v l r) =
  foldlMap f (f (foldlMap f i l) k v) r

size :: Map k v -> Int
size = foldMap (\ a k v -> a + 1) 0

toList :: Ord k => Map k v -> [(k, v)]
toList = reverse . foldlMap (\ a k v -> (k, v) : a) []

union :: Ord k => Map k v -> Map k v -> Map k v
union m1 m2 = foldlMap (\ a k v -> insert k v a) m1 m2
(Created using Swinx, RunestoneComponents and PetljaDoc)
© 2022 Petlja
A- A+