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

Функциональное программирование. Конспект лекций

.pdf
Скачиваний:
0
Добавлен:
15.08.2026
Размер:
1 Мб
Скачать

которое «сохраняется» в монаде State. Для запуска процесса необходимо задать начальное состояние!!!

8.5. Mонада ST

Существуют алгоритмы, которые проще всего реализуются с использованием изменяемых значений переменных. Однако обновление значения является побочным эффектом, так как появляются фазы: до обновления и после обновления. Если же обновление происходит локально, меняется только одно значение, за время обновления ни одна другая переменная не может пользоваться промежуточными значениями, и обновления происходят с помощью чистых функций, то такую функцию можно считать чистой. Для реализации изменяемых значений и обеспечения чистоты функций имеется «монадический» тип ST, по структуре похожий на монаду State, но для хранения изменяемого значения используется монада STRef. Причем типы ST и STRef имеют общий параметр состояния s: ST s (STRef s a). Начальное значение состояния s берется «из воздуха». В Haskell специально для этого типа сделано исключение. Это чисто фиктивный параметр, он скрыт от пользователя и нигде не может использоваться.

Тип ST (State Transformer) является экземпляром Monad и определен в модуле Control.Monad.ST. У него два параметра и нет конструкторов [1]:

data ST s a.

Для хранения значений используется тип Data.STRef (ST reference). Основные функции для работы с хранимыми значенями:

newSTRef :: a -> ST s (STRef s a) readSTRef :: STRef s a -> ST s a writeSTRef :: STRef s a -> a -> ST s ().

Такие функции иногда называют смышлеными конструкторами (smart constructors), так как они позволяют строить значение, но скрывают от пользователя реализацию за счет скрытия конструкторов типа (модуль экспортирует лишь имя типа STRef).

С помощью этих функций можно реализовать функцию обновления значения по ссылке (функцию из модуля Data.STRef):

– 91 –

modifySTRef :: STRef s a -> (a -> a) -> ST s () modifySTRef ref f = writeSTRef ref . f =<< readSTRef ref.

Так же, как и для State, для ST определены экземпляры классов

Functor, Applicative и Monad. Функция runST возвращает «чистое» значение из монады:

runST :: (forall s. ST s a) -> a.

Слово forall означает «для любых». Это слово называется квантором всеобщности и говорит о том, что функция извлечения значения не может делать никаких предположений о типе фиктивного состояния. Функция runST забывает тип фиктивного состояния s из типа ST, но в типе STRef этот параметр строго задан. По типу переменной v :: STRef s a компилятор может сказать, что существует такое s, для которого значение v имеет смысл (правильно типизировано). Но оно не любое! Тем самым компилятор будет следить за «чистотой» обновлений. С помощью монады ST s (STRef s a) можно реализовать циклы, аналогичные циклам в императивных языках программирования. Например, цикл for:

import Control.Monad import Data.STRef import Control.Monad.ST

forLoop :: i -> (i -> Bool) -> (i -> i) -> (i -> s -> s) -> s -> s

forLoop i0 pred next update s0 = runST $ do refI <- newSTRef i0

refS <- newSTRef s0 iter refI refS readSTRef refS

where iter refI refS = do i <- readSTRef refI s <- readSTRef refS when (pred i) $ do

writeSTRef refI $ next i writeSTRef refS $ update i s iter refI refS

– 92 –

Параметрами функции forLoop являются:

i0 – начальное значение индекса,

pred – предикат окончания цикла,

next – функция увеличения индекса,

update – функция изменения состояния s,

s0 – начальное состояние.

8.5.1. Монада изменяемых массивов STArray

В Haskell есть несколько типов изменяемых массивов [1]. Следующий класс определяет общий интерфейс к различным массивам:

class (HasBounds a, Monad m) => MArray a e m where newArray :: Ix i => (i, i) -> e -> m (a i e)

newArray_ :: Ix i => (i, i) -> m (a i e).

Метод newArray создает массив типа a, который «завернут» в тип-монаду m. (i, i) – диапазон значений индексов массива, e – элемент, который будет записан во все ячейки массива. Для работы с массивом используются функции:

readArray :: (MArray a e m, Ix i) => a i e -> i -> m e writeArray :: (MArray a e m, Ix i) => a i e -> i -> e -> m ().

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

runSTArray :: Ix i => (forall s . ST s (STArray s i e)) -> Array i e.

Для создания изменяемого монадного массива типа ST s (STArray s i e) из списка необходимо использовать функцию

newListArray (i, j) list,

где (i,j) – диапазон индексов массива, list – список.

Ниже приводится пример создания изменяемого массива из списка, запись нового значения элемента и печать элементов массива.

import Control.Monad import Data.STRef import Control.Monad.ST

– 93 –

import Data.Array import Data.Array.ST

createArr::[Int] -> ST s (STArray s Int Int) createArr xs = do

arr <- newListArray (0, length xs - 1) xs writeArray arr 0 777

return arr printArr::[Int] -> [Int]

printArr xs = elems $ runSTArray $ arr where arr = createArr xs

В результате выполнения функции printArr [1,2,3,4,5] будет получено

*Qsort> printArr [1,2,3,4,5] [777,2,3,4,5]

Как видно, значение нулевого элемента массива было заменено на 777.

Ниже представлен фрагмент кода программы быстрой сортировки массива с использованием «монадического» типа ST s (STArray s i e):

import Control.Monad import Data.STRef import Control.Monad.ST import Data.Array import Data.Array.ST

forLoop :: i -> (i -> Bool) -> (i -> i) -> (i -> s -> s) -> s -> s

forLoop i0 pred next update s0 = runST $ do refI <- newSTRef i0

refS <- newSTRef s0 iter refI refS readSTRef refS

– 94 –

where iter refI refS = do i <- readSTRef refI s <- readSTRef refS when (pred i) $ do

writeSTRef refI $ next i writeSTRef refS $ update i s iter refI refS

swapElems :: Ix i => i -> i -> STArray s i e -> ST s ()

swapElems i j arr = do

vi <- readArray arr i vj <- readArray arr j writeArray arr i vj writeArray arr j vi

qsortST :: Ord a => Int -> Int -> (forall s. STArray s Int Int) -> ST s ()

qsortST left right arr = do when (left <= right) $ do

swapElems left (div (left + right) 2) arr vLeft <- readArray arr left

(last, _) <- forLoop (left + 1) (<= right) succ (update vLeft) (return (left, arr))

swapElems left last arr qsortST left (last - 1) arr qsortST (last + 1) right arr where update vLeft i st = do

(last, arr) <- st

vi <- readArray arr i if (vi < vLeft)

then do

swapElems (succ last) i arr return (succ last, arr)

else do

return (last, arr) qsort :: Ord a => [a] -> [a]

– 95 –

qsort xs = elems $ runSTArray $ do

arr <- newListArray (left, right) xs qsortST1 left right arr

return arr where left = 0

right = length xs - 1

Результат работы программы сортировки выглядит следующим образом:

*Qsort> qsort [5,3,1,8,4] [1,3,5,8,4]

– 96 –

Лекция 9. Отладка в Haskell

Рассматривается технология и особенности отладки программ в Haskell. Описываются шаги и действия разработчика при отладке программ.

Ключевые слова: отладчик, debug, точки прерывания, trace.

Введение. По сравнению с современными средами разработки ПО, такими как Microsoft Visual Studio, Eclipse, PyCharm, Spyder и

другие, режим отладки в Haskell не очень удобен. Отладчик интегрирован в интерпретатор GHCi, включается по умолчанию и позволяет выполнять пошаговое выполнение программы и анализировать на каждом шаге значение связанных переменных. Точки останова (breakpoints) и пошаговая отладка доступны только в режиме интерпретатора. Скомпилированный код недоступен отладчику.

Итак, отладчик обеспечивает следующую функциональность: установка точек прерывания программ (breakpoints) на опреде-

лениях функций или на выражениях;

пошаговая отладка (single-stepped) программы с остановкой после каждой редукции выражений и с возможностью анализа значений связанных переменных;

трассировка программы (tracing mode), при которой интерпретатор запоминает каждый шаг редукции, но не останавливает вычисления до достижения точки прерывания (breakpoint);

трактовка исключений (ошибки сопоставления, error) как точек прерывания, что позволяет локализовать место исключения в программе.

9.1. Установка точек прерывания

Для установки точек прерывания желательно знать номера строк файла, в которых записана та или иная функция. Напомним, что программа на языке Haskell – это обычный текстовый файл (удобнее использовать Notepad++ вместо обычного блокнота). Поэтому после загрузки программы необходимо выдать команду

:list func,

– 97 –

для того чтобы получить информацию о номерах строк функции func; например, чтобы получить информацию о расположении функции qsort, дадим команду :list qsort:

*TestTrace> :list qsort

4

5qsort [] = []

6qsort (a:as) = trace ("left:" ++ show left) (qsort left) ++ [a] ++ trace ("right:" ++ show right) (qsort right)

7where (left,right) = (filter (<=a) as, filter (>a) as)

В результате можно увидеть, что функция qsort располагается в строках 5-7.

Для установки точек прерывания в режиме интерпретатора можно ввести следующие команды:

:break identifier :break line :break line column :break module line

:break module line column.

В качестве identifier указывается имя отлаживаемой функции. Параметры line и column указывают на номер строки и столбца выражения. Если имя модуля (module) не указано, то подразумевается последний загруженный модуль.

Просмотреть установленные точки прерывания можно с помощью команды

:show breaks.

Например, в приведенном ниже примере расставлены две точки прерывания в строках 5 и 6:

*TestTrace> :break 5

Breakpoint 0 activated at testTrace.hs:5:12-13 *TestTrace> :break 6

– 98 –

Breakpoint 1 activated at testTrace.hs:6:16-111 *TestTrace> :show breaks

[0]TestTrace testTrace.hs:5:12-13

[1]TestTrace testTrace.hs:6:16-111

Порядковые номера точек прерывания указаны в квадратных скобках (в данном случае указаны две точки прерывания [0] и [1]).

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

:delete n,

где n – номер точки прерывания.

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

:delete *.

9.1.1. Вложенные точки прерывания

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

:step qsort [1,3],

которое запускает новую вложенную точку прерывания. Эта новая точка прерывания становится «текущей», а старая сохраняется в стеке. Таким образом, можно создать произвольное количество контекстов точек прерывания.

9.1.2. Переменная _result

При остановке в точке прерывания отладчик связывает переменную _result со значением текущего активного выражения. Значение _result может быть не доступно, так как была остановка его оценки. Однако если тип _result известен и доступен для отображения, то, просто введя _result в приглашении, можно увидеть его значение. Однако выполнение _result может вызвать дальнейшие точки прерывания, начиная с точки прерывания, на которой произошла остановка в данный момент. Поэтому необходимо будет выполнить команду :continue сразу после оценки _result.

– 99 –

9.2. Пошаговая отладка

Для пошаговой отладки имеются команды:

:step [expr] :steplocal [expr] :stepmodule [expr].

Команда :step используется, чтобы включить все точки прерывания в программе, и выполняется, пока не будет достигнута следующая точка прерывания (параметры, указанные в квадратных скобках, могут отсутствовать).

Команда :steplocal ограничивает набор разрешенных точек прерывания точками в текущей функции верхнего уровня.

Команда :stepmodule используется для пошагового выполнения только для точек прерывания, содержащихся в текущем модуле.

Команда :step expr выполняет оценку выражения expr в одношаговом режиме (single-stepping mode). Если выражение expr опущено, то выполняется один шаг от текущей точки останова.

Для определения точки прерывания в программе после очередного шага используется команда :list. Интерпретатор выделяет специальными значками типа «^» те выражения, которые редуцируются на данном шаге. Чтобы выделение было корректным, очень важно при написании программы при создании отступов, блоков do или where использовать пробел, а не знак табуляции. Кроме того, при добавлении новой инструкции в середину блока do или where редактор, как правило, выравнивает инструкции с помощью табуляции, приводя к тому, что Haskell считает эту инструкцию уже не находящейся в блоке, и выдает сообщение об ошибке!

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

:set stop :list

:set -fprint-evld-with-show.

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

:force var

вместо команды

:print var

– 100 –

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