В отличие от функционального программирования, где значение вычисляется посредством применения функции к аргументам, не заботясь о том как это происходит, императивное программирование ближе к машинному представлению, так как оно вводит понятие состояния памяти, которое изменяется под воздействием программы. Каждое такое воздействие называется инструкцией и императивная программа есть набор упорядоченных инструкций. Состояние памяти может быть изменено при выполнении каждой инструкции. Операции ввода/вывода можно рассматривать как изменение оперативной памяти, видео памяти или файлов.
Подобный стиль программирования напрямую происходит от программирования на ассемблере. Мы встречаем этот стиль в языках программирования первого поколения (Fortran, C, Pascal, etc). Следующие элементы Objective CAML соответствуют приведенной модели:
Некоторые алгоритмы реализуются проще таким стилем программирования. Примером может послужить произведение двух матриц. Несмотря на то что эту операцию можно реализовать чисто функциональным способом, при этом списки заменят массивы, это не будет ни эффективнее, ни естественней по отношению к императивному стилю.
Интерес интеграции императивной модели в функциональный язык состоит в написании при необходимости подобных алгоритмов в этом стиле программирования. Два главных недостатка императивного программирования по отношению к функциональному это:
Однако, при соблюдении некоторых правил написания программ, выбор стиля программирования предоставляет большие возможности в написании алгоритмов, что является главной целью языков программирования. К тому же, программа написанная в стиле близкому к стилю алгоритма, имеет больше шансов быть корректной (или по крайней мере быстрее реализована).
По этим причинам в Objective CAML имеются типы данных, значения которых физически изменяемые, структуры контроля выполнения программ и библиотека ввода/вывода в императивном стиле.
В данной главе продолжается представление базовых элементов Objective CAML затронутых в предыдущей главе, но теперь мы заинтересуемся императивными конструкциями. Глава разбита на 5 разделов. Первый, наиболее важный, раскрывает различные физически изменяемые структуры данных и описывает их представление в памяти. Во втором разделе кратко излагаются базовые операции ввода/вывода. Третий знакомит с новыми итеративными структурами контроля. В четвертом разделе рассказывается об особенностях выполнения императивных программ, в частности о порядке вычисления аргументов функции. В последнем разделе мы переделаем калькулятор предыдущей главы в калькулятор с памятью.
Значения следующих типов: массивы, строки, записи с модифицируемыми полями и указатели есть структурированные значения, компоненты которых могут быть физически изменены.
Мы уже видели, что переменная в Objective CAML привязана к значению и хранит эту связку до конца ее существования. Мы можем изменить связку лишь переопределением, но и в этом случае речь не будет идти о той же самой переменной. Новая переменная с тем же именем скроет предыдущую, которая перестанет быть доступной напрямую и останется неизменной. В случае с изменяемыми переменными, мы можем ассоциировать новое значение переменной без ее переопределения. Значение переменной доступно в режиме чтение/запись.
Векторы или одномерные массивы группируют определенное число однотипных элементов. Существует несколько способов создания векторов, первый — перечисление элементов, разделенных запятой и заключенных между символами [| и |] .
Функция Array.create с двумя аргументами на входе: размер вектора и начальное значение, возвращает новый вектор.
Для того чтобы изменить или просто просмотреть значение элемента необходимо указать его номер.
Синтаксис
Синтаксис
expr1 должен быть вектором (тип array) с элементами типа expr3. Конечно, выражение expr2 должно быть типа int. Результат модификации есть выражение типа unit. Номер первого элемента вектора 0 и последнего — размер вектора минус 1. Скобки вокруг выражения обязательны.
Если индекс элемента не принадлежит интервалу индексов вектора, то возбуждается исключения в момент доступа.
Эта проверка осуществляется в момент выполнения программы, что может сказаться на ее быстроте. Однако, это необходимо для избежания заполнения зоны памяти вне вектора, что может привести к серьезным ошибкам.
Функции манипуляции векторами являются частью модуля Array стандартной библиотеки, описание которого дано в главе 7 на стр. ??. В следующих примерах мы воспользуемся тремя функциями из этого модуля:
Все элементы вектора содержат значение, переданное во время создания вектора. В случае, если это значение структурное, оно будет совместно использоваться (sharing). Создадим, для примера, матрицу как вектор векторов при помощи функции Array.create:
Если поменять одно из полей вектора v, который мы использовали для создания m, мы изменим все строки матрицы (см. рисунки 2.1 и 2.2).
Если значение инициализации вектора (второй аргумент функции Array.create) простое, атомарное, то оно копируется каждый раз и совместно используется если это значение структурное.
Мы называем атомарным значением то, размер которого не превышает стандартный размер значений Objective CAML, то есть memory word — целое, символ, булевый и конструкторы констант. Другие значения, называемые структурные, представлены указателем на зону памяти. Эта разница объяснена более детально в главе 8 стр. ??.
Векторы чисел с плавающей запятой есть особый случай. Несмотря на то что эти числа (float) — структурные значения, при создании вектора начальное значение копируется. Делается это для оптимизации. В главе 11 стр. ??, в которой обсуждается интерфейс с языком C мы обсудим эту проблему.
Матрица, вектор векторов, не обязательно должна быть квадратной. Действительно, мы можем заменить вектор на другой вектор с иным размером, что удобно для ограничения размера матрицы. Следующее значение t создает треугольную матрицу для коэффициентов треугольника Паскаля.
В этом примере элемент вектора t по индексу i есть вектор целых чисел размером i+1. Для манипуляции такой матрицей необходимо знать размер каждого элемента вектора.
При копировании вектора или конкатенации двух векторов мы получаем новый вектор. Модификация исходных векторов не повлияет на значение копий, кроме случая разделения значений рассмотренного ранее.
В приведенном примере видно что копия m содержит лишь указатели на v, если один из элементов v изменен, то m2 тоже будет изменен. Конкатенация создает новый вектор длина которого равна сумме длин двух других.
Однако, изменение v, разделяемого m и mm, приведет к изменению обоих матриц:
Строки можно рассматривать как частный случай массива символов. Однако, по причинам использования памяти этот тип специализирован и синтаксис доступа к элементам изменен.
Синтаксис
Элементы строки могут быть физически изменены.
Синтаксис
Поля записи могут быть объявлены изменяемыми. Для этого достаточно указать при декларации типа поля ключевое слово mutable
Синтаксис
Пример определения точки плоскости.
Для изменения значение поля объявленного mutable используйте следующий синтаксис.
Синтаксис
Выражение expr1 должно быть типа запись содержащее поле nom. Операция модификации возвращает значение типа unit.
Напишем функцию перемещения точки, которая изменяет ее координаты. Здесь мы используем локальную декларацию с фильтражом, для упорядочения побочных эффектов.
Мы можем определить изменяемые или не изменяемые поля, только поля, помеченные ключевым словом mutable, будут изменяемые.
Далее, мы приводим пример использование записей с изменяемыми полями и массивов для того чтобы реализовать структуру стека (см 2.3.3 стр. ??).
Objective CAML предлагает полиморфный тип ref, рассматриваемый как указатель на любое значение. В Objective CAML указатель точнее будет назвать указатель на значение. Указываемое значение может быть изменено. Тип ref определен как запись с изменяемым полем.
Создать ссылку на значение можно функцией ref. Указываемое значение может быть получено использованием префикса !. Для модификации значения используем инфиксную функцию (:=).
Тип ref параметризованный (1.2.6), что позволяет создавать указатели на любой тип. Однако, существует несколько ограничений. Представим, что нет никаких ограничений и мы можем объявить
В этом случае тип переменной х будет 'a list ref и можем изменить значение способом не соответствующим статической типизации Objective CAML.
При этом получаем одну и ту же переменную типа int list в один момент и bool list в другой.
Во избежании подобной ситуации, система типов Objective CAML использует новую категорию типа переменной — слабо типизированные переменные. Синтаксически, они отличаются предшествующим символом подчеркивания.
Переменная типа '_a не является параметром типа, то есть ее тип не известен до момента ее реализации. Лишь в момент первого использования x, после объявления, тип будет окончательно зафиксирован.
Таким образом тип переменной x int list ref.
Тип, содержащий неизвестную, является мономорфным, не смотря на то что его тип еще не был указан и невозможно реализовать то что неизвестно полиморфным типом.
В этом примере, не смотря на то что мы реализуем неизвестную типом a priori полиморфным ('a->unit), тип остался мономорфным с новой неизвестной типа.
Это ограничение полиморфизма распространяется не только на указатели, но и на любое значение содержащее изменяемую часть: массивы, записи с полями объявленные mutable, и так далее. Все параметры типа, даже не имеющие никакого отношения к модифицируемым атрибутам, есть переменные слабого (weak) типа.
Warning
Эта модификация типа повлияет на чисто функциональные программы.
Если к полиморфной переменной применена полиморфная функция, то в результате мы
получим переменную слабого типа, так как нельзя не учитывать что функция может
создать физически изменяемое значение. Иными словами, результат будет всегда
мономорфный.
Тот же результат мы получим для частичных функций.
Для получения полиморфного типа необходимо вычесть второй аргумент f и применить ее:
Действительно, выражения определяющее x есть функциональное выражение function x -> f 1 x. Его вычисление даст замыкание, которое не рискует произвести побочный эффект, так как тело функции не вычислено.
В общем случае мы делаем различие между т.н. "не расширяемыми" выражениями, которые не вызывают побочных эффектов при вычислении и между "расширяемыми" выражениями. Система типов Objective CAML классифицирует выражения в соответствии с синтаксисом:
Функции в/в вычисляют значение (чаше всего типа unit), однако в ходе вычисления, они изменяют состояние устройств в/в: изменение буфера клавиатуры, перерисовка экрана, запись в файл и так далее. Для каналов ввода и вывода определены два типа: in_channel и out_channel. При обнаружении конца файла возбуждается исключение End_of_file. Следующие константы соответствуют стандартным каналам ввода, вывода и ошибок a la Unix: stdin, stdout и stderr.
Функции ввода/вывода стандартной библиотеки Objective CAML, манипулируют каналами типа in_channel и out_channel соответственно. Для создания подобных каналов используется функция:
open_in открывает файл2, если он не существует, возбуждается исключение Sys_error.
open_out создает указанный файл если такой не существует, если же он существует то его содержимое уничтожается.
Функции закрытия каналов:
Стандартные функции чтения/записи:
Следующие функции чтения (записи) из (в) стандартного входа:
Другие значения простых типов данных могут быть прочтены/записаны, значения таких типов могут быть переведены в тип список символов.
Попробуем вывести на экран выражение формы let x=e1 in e2. Зная, что в общем случае локальная переменная x может быть использована в e2, выражение e1 вычислено первым и только затем e2. Если эти оба выражения есть императивные функции с побочным эффектом результат которых (), мы выполнили их в правильном порядке. В частности, так как нам известен тип возвращаемого результата e1: константа () типа unit, мы получим последовательный вывод на экран используя сопоставление с обтазцом ().
В следующем примере мы приводим игру Больше/Меньше, состоящую в поиске числа, после каждого ответа программа выводит на экран сообщение в соответствии с тем что предложенное число больше или меньше загаданного.
Результат запуска
Операции ввода/вывода и изменяемые значения имеют побочный эффект. Их использование упрощено императивным стилем программирования с новыми структурами контроля. В этом параграфе мы поговорим о последовательных и итеративных структурах.
Нам уже приходилось встречать (см. 1.1.2 стр. ??) условную структуру контроля if then смысл которой такой же как и в императивных языках программирования.
Пример
Самая типичная императивная структура — последовательность, которая позволяет вычислять выражения разделенные точкой с запятой в порядке слева направо.
Синтаксис
Последовательность — это выражение, результат которого есть результат последнего выражения (здесь exprn). Все выражения вычислены и побочный эффект каждой учтен.
С побочным эффектом, мы получаем обычные конструкции императивного программирования.
В связи с тем что значения предшествующие точке запятой не сохранены, Objective CAML выводит предупреждение в случае если их тип отличен от типа unit.
Чтобы избежать этого сообщения, можно воспользоваться функцией ignore.
Другое сообщение будет выведено в случае если забыт аргумент функции
Чаще всего мы используем скобки для определения зоны видимости. Синтаксически расстановка скобок может принимать 2 формы:
Синтаксис
Синтаксис
Теперь мы можем написать Больше/Меньше простым стилем (стр. ??).
Структуры итеративного контроля тоже не принадлежат “миру” функционального программирования. Условие повторения цикла или выхода из него имеет смысл в случае когда есть физическое изменение памяти. Существует две структуры итеративного контроля: цикл for для ограниченного количества итераций и while для неограниченного. Структуры цикла возвращают константу () типа unit.
Цикл for может быть возрастающим (to) или убывающим (downto) с шагом в одну единицу.
Синтаксис
Тип выражений expr1 и expr2 — int. Если тип expr3 не unit, компилятор выдаст предупреждение.
Неограниченный цикл while имеет следующий синтаксис
Тип выражения expr1 должен быть bool. И, как в случае for, если тип expr2 не unit, компилятор выдаст предупреждение.
Необходимо помнить, что циклы — это такие же выражения как и любые другие, они вычисляют значение () типа unit.
Обратите внимание на то, что строка “- end\n” выводится после цифр 1…10: подтверждение того что аргументы (в данном случае цикл) вычисляются перед передачей функции.
В императивном стиле тело цикла (выражение expr) не возвращает результата, а производит побочный эффект. В Objective CAML, если тип тела цикла не unit, то компилятор выдает сообщение :
В нашем примере структура данных 'a stack будет реализована в виде записи с 2 полями: структура данных stack будет запись с двумя полями: массив элементов и индекс первой свободной позиции в массиве.
В поле size будет хранится максимальный размер стека.
Операции над стеком будут init_stack для инициализации, push для добавления и pop для удаления элемента.
Эта функция не может создавать не пустой массив, так для создания массива необходимо передать элемент. Поэтому поле elts получает пустой массив.
Два исключения добавлены на случай попытки удалить элемент из пустого стека и добавить элемент в полный. Они используются в функциях pop и push.
Небольшой пример использования:
Чтобы избежать исключения Stack_full при переполнении стека, мы можем каждый раз увеличивать его размер. Для этого нужно чтобы поле size было модифицируемым.
Однако необходимо быть осторожным со структурами которые могут беспредельно увеличиваться. Вот небольшой пример стека, увеличивающегося по необходимости.
В функции pop желательно добавить возможность уменьшения размера стека, это позволит сэкономить память.
В этом примере мы определим тип “матрица” — двумерный массив чисел с плавающей запятой и несколько функций. Мономорфный тип mat это запись, состоящая из размеров и элементов матрицы. Функции create_mat, access_mat и mod_mat служат для создания, доступа и изменения элементов матрицы.
Сумма двух матриц a и b есть матрица c, такая что
cij=aij+bij
Произведение двух матриц a и b есть матрица c, такая что cij=∑k=1m aik*bki
В функциональном языке программирования порядок вычисления аргументов не имеет значения. Из–за того что нет ни изменения памяти, ни приостановки вычисления, расчет одного аргумента не влияет на вычисление другого. Objective CAML поддерживает физически изменяемые значения и исключения, поэтому пренебречь порядком вычисления аргументов нельзя. Следующий пример специфичен для Objective CAML 2.04 ОС Linux на платформе Intel:
По выводу на экран мы видим что вторая строка печатается после первой.
Таков же результат для исключений:
Если необходимо указать порядок вычисления аргументов, необходимо использовать локальные декларации, форсируя таким образом порядок перед вызовом функции. Предыдущий пример может быть переписан следующим способом:
В Objective CAML порядок вычисления не указан, на сегодняшний день все реализации caml делают это слева направо. Однако рассчитывать на это может быть рискованно, в случае если в будущем язык будет реализован иначе.
Это вечный сюжет дебатов при концепции языка. Нужно ли специально не указывать некоторые особенности языка и предложить программистам не пользоваться ими, иначе они рискуют получить разные результаты для разных компиляторов. Или же необходимо их указать и, следовательно, разрешить программистам ими пользоваться, что усложнит компилятор и сделает невозможным некоторые оптимизации?
Вернемся к нашему примеру с калькулятором описанным в предыдущей главе и добавим ему пользовательский интерфейс. Теперь мы будем вводить операции напрямую и получать результат сразу без вызова функции транзиций (из одного состояния в другое) при каждом нажатии на кнопку.
Добавим 4 новые кнопки: C которая очищает экран, M для сохранения результата в памяти, m для его вызова из памяти и OFF для выключения калькулятора. Что соответствует следующему типу:
Теперь определим функцию, переводящую введенные символы в значение типа key. Исключение Invalid_key отлавливает все символы, которые не соответствуют кнопкам калькулятора. Функция code модуля Char переводит символ в код ASCII.
В императивном стиле, функция перехода (transition) не приведет к новому состоянию, а физически изменит текущее состояние калькулятора. Таким образом необходимо изменить тип state так, чтобы его поля были модифицируемые. Наконец, определим исключение Key_off для обработки нажатия на кнопку OFF.
Определим функцию запуска калькулятора go, которая возвращает (), так как нас интересует только ее эффект на окружение (ввод/вывод, изменение состояния). Ее аргумент есть константа (), так как наш калькулятор автономен (он сам определяет свое начальное состояние) и интерактивен (данные необходимые для расчета вводятся с клавиатуры по мере необходимости). Транзиции реализуются в бесконечном цикле (while true do) из которого мы можем выйти при помощи исключения Key_off.
Заметим, что начальное состояние должно быть либо передано в параметре, либо объявлено локально внутри функции go, для того чтобы оно каждый раз инициализировалось при запуске этой функции. Если бы мы использовали значение initial_state в функциональном стиле, калькулятор начинал бы работать со старым состоянием, которое он имел перед выключением. Таким образом было бы не просто использовать два калькулятора в одной программе.
В этой главе вы видели применение стилей императивного программирования (физически изменяемые значения, ввод–вывод, структуры итеративного контроля) в функциональном языке. Только mutable значения, такие как строки, векторы и записи с изменяемыми полями могут быть физически изменены. Другие значения не могут меняться после их создания. Таким образом мы имеем значения read–only для функциональной части и значения read–write для императивной.
Нужно отметить, что в случае отказа от использования императивных возможностей языка, это расширение функционального “ядра” языка не меняют его возможностей функционального программирования, за исключением некоторых, легко решаемых проблем с типами.