Previous Up Next

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

Введение

Первый язык функционального программирования LISP появился в конце 1950, в тот же момент, что и Fortran – один из первых императивных языков. Оба этих языка существуют и по сей день, хотя они немало изменились. Область их применения: вычислительные задачи для Фортрана и символьные (symbolic) для Lisp. Интерес к функциональному программированию состоит в простоте написания программ, где под программой подразумевается функция, примененная к аргументам. Она вычисляет результат, который возвращается как вывод программы. Таким образом можно с легкостью комбинировать программы: вывод одной, становится входным аргументом для другой.

Функциональное программирование основывается на простой модели вычислений, состоящей из трех конструкций: переменные, определение функции и ее применение к какому–либо аргументу. Эта модель, называемая λ–исчисление, была введена Alonzo Church в 1932, еще до появления первых компьютеров. В λ–исчислении любая функция является переменной, так что она может быть использована как входной параметр другой функции, или возвращена как результат другой. Теория λ-исчисления утверждает что все то, что вычисляемо может быть представлено этим формализмом. Однако, синтаксис этой теории слишком ограничен, чтобы его можно было использовать его как язык программирования. В связи с этим к λ-исчислению были добавлены базовые типы (например, целые числа или строки символов), операторы для этих типов, управляющие структуры и объявление позволяющие именовать переменные или функции, и в частности рекурсивные функции.

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

План главы

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

В первоm разделе описаны основы языка, начиная с базовых типов и функций. Затем мы рассмотрим структурные и функциональные типы. После этого мы рассмотрим управляющие структуры, а также локальные и глобальные объявления. Во втором разделе мы обсудим определение типов для создания структур и механизм сопоставления с образцом, который используется для доступа к этим структурам. В третьем разделе рассматривается выводимый тип функций и область их применения, после чего следует описание механизма исключений. Четвертый раздел объединяет введенные понятия в простой пример программы-калькулятора.

1.1  Функциональное ядро Objective CAML

Как любой другой язык функционального программирования, Objective CAML — это язык выражений, состоящий в основном из создания функций и их применения. Результатом вычисления одного из таких выражений является значение данного языка (value in the language) и выполнение программы заключается в вычислении всех выражений из которых она состоит.

1.1.1  Значения, функции и базовые типы

В Objective CAML определены следующие типы: целые числа, числа с плавающей запятой, символьный, строковый и логический.

Числа

Различают целые int и числа с плавающей запятой float. Objective CAML следует спецификации IEEE 754 для представления чисел с плавающей запятой двойной точности. Операции с этими числами описаны в таблице 1.1. Если результат целочисленной операции выходит за интервал значений типа int, то это не приведет к ошибке. Результат будет находиться в интервале целых чисел, то есть все действия над целыми ограниченны операцией modulo с границами интервала.


целыеплавающие
+ сложение+. сложение
- вычитание и унарный минус-. вычитание и унарный минус
* умножение*. умножение
/ деление/. деление
mod остаток целочисленного деления** exponentiation


# 1 ;;# 2.0 ;;
- : int = 1- : float = 2
# 1 + 2 ;;# 1.1 +. 2.2 ;;
- : int = 3- : float = 3.3
# 9 / 2 ;;# 9.1 /. 2.2 ;;
- : int = 4- : float = 4.13636363636
# 11 mod 3 ;;# 1. /. 0. ;;
- : int = 2- : float = Inf
(* лимит представления *)(* лимит представления *)
(* целых чисел *)(* с плавающей запятой *)
# 2147483650 ;;# 222222222222.11111 ;;
- : int = 2- : float = 222222222222
Table 1.1: Операции над числами

Разница между целыми числами и числами с плавающей запятой.

Значения разных типов, таких как float и int, не могут сравниваться между собой напрямую. Для этого существует функции перевода одного типа в другой (float_of_int и int_of_float).

2=2.0;; This expression has type float but is here used with type int 3.0=float_of_int 3;; - : bool = true

Аналогично, операции над целыми числами и числами с плавающей запятой различны:

3+2;; - : int = 5 3.0+.2.0;; - : float = 5 3.0+2.0;; Characters 0-3: This expression has type float but is here used with type int # sin 3.14159;; - : float = 2.65358979335e-06

Неопределенный результат, например получаемый при делении на ноль, приведет к возникновению исключения (см. 1.3, стр. ??), которое остановит вычисление. Числа с плавающей запятой имеют специальные значения для бесконечных величин (Inf) и для не определенного результата (NaN1). Основные операции над этими числами приведены в таблице 1.2


ф-ии над числами с плавающей запятойтригонометрические функции:
ceilcos косинус
floorsin синус
sqrt - квадратный кореньtan тангенс
exp - экспонентаacos арккосинус
log - натуральный логарифмasin арксинус
log10 - логарифм по базе 10atan арктангенс


# ceil 3.4 ;;# sin 1.57078 ;;
- : float = 4- : float = 0.999999999867
# floor 3.4 ;;# sin (asin 0.707) ;;
- : float = 3- : float = 0.707
# ceil (-.3.4) ;;# acos 0.0 ;;
- : float = -3- : float = 1.57079632679
# floor (-.3.4) ;;# asin 3.14 ;;
- : float = -4- : float = NaN
Table 1.2: Функции над числами с плавающей запятой

Символы и строки

Символы, тип char, соответствуют целым числам в интервале от 0 до 255, первые 128 значений соответствуют кодам ASCII. Функция char_of_int и int_of_char преобразуют один тип в другой. Строки, тип string — это последовательность символов определенной длинны (не длиннее 224−6). Оператором объединения строк (конкатенации) является шапка '^'. Следующие функции необходимы для перевода типов int_of_string, string_of_int, string_of_float и float_of_string.

'B' ;; - : char = 'B' # int_of_char 'B' ;; - : int = 66"est une chaîne" ;; - : string = "est une cha\\238ne" (string_of_int 1987) ^ " est l'année de la création de CAML" ;; - : string = "1987 est l'ann\233e de la cr\233ation de CAML"

Если строка состоит из цифр, то мы не сможем использовать ее в численных операциях, не выполнив явного преобразования.

"1999" + 1 ;; Characters 1-7: This expression has type string but is here used with type int (int_of_string "1999") + 1 ;; - : int = 2000

В модуле String собрано много функций для работы со строками (стр. 7.3.2)

Булевый тип

Значение типа boolean принадлежит множеству состоящему из двух элементов: true и false. Основные операторы описаны в таблице  1.3. По историческим причинам операторы and и or имеют две формы.


not - отрицание 
&& последовательное и& синоним &&
|| последовательное или or синоним |
Table 1.3: Булевы операторы

# true ;; - : bool = true # not true ;; - : bool = false # true && false ;; - : bool = false

Операторы && и || или их синонимы, вычисляют аргумент слева и в зависимости от его значения, вычисляют правый аргумент. Они могут быть переписаны в виде условной структуры (см. 1.1.2 стр. ??).


= равенство структурное< меньше
== равенство физическое> больше
<> отрицание =<= меньше или равно
!= отрицание ==>= больше или равно
Table 1.4: Операторы сравнения и равенства

Операторы сравнения и равенства описаны в таблице 1.4. Это полиморфные операторы, то есть они применимы как для сравнения двух целых, так и двух строк. Единственно ограничение это то что операнды должны быть одного типа (см. 1.1.5 стр.  ??).

1<=118 && (1=2 || not(1=2)) ;; - : bool = true 1.0 <= 118.0 && (1.0 = 2.0 || not (1.0 = 2.0)) ;; - : bool = true "un" < "deux" ;; - : bool = false 0 < '0' ;; Characters 4-7: This expression has type char but is here used with type int

Структурное равенство, при проверке двух переменных, сравнивает значение полей структуры, тогда как физическое равенство проверяет занимают ли эти переменные одно и то же место в памяти. Оба оператора возвращают одинаковый результат для простых типов: boolean, char, int и константные конструкторы (см. 1.2.4 стр. ??).


Warning
числа с плавающей запятой и строки рассматриваются как структурные типы.

Unit

Тип unit определяет множество из всего одного элемента, значение которого: ()

() ;; - : unit = ()

Это значение будет широко использоваться в императивных программах (см. 2 стр. ??), в функциях с побочным эффектом. Функции, результат которых равен (), соответствуют понятию процедуры, которое отсутствует в Objective CAML, так же как и аналог типа void в языке C.

Декартово произведение, кортежи

Значения разных типов могут быть сгруппированы в кортежи. Значения из которых состоит кортеж разделяются запятой. Для конструкции кортежа, используется символ *. int*string есть кортеж, в котором первый элемент целое число и второй строка.

v

( 12 ,"octobre" ) ;; - : int * string = 12, "octobre"

Иногда мы можем использовать более простую форму записи.

12 , "octobre" ;; - : int * string = 12, "octobre"

Функции fst и snd дают доступ первому и второму элементу соответственно.

# fst ( 12 , "octobre" ) ;; - : int = 12 # snd ( 12 , "octobre" ) ;; - : string = "octobre"

Эти обе функции полиморфные, входной аргумент может быть любого типа.

# fst;; - : 'a * 'b -> 'a = <fun> # fst ( "octobre", 12 ) ;; - : string = "octobre"

Тип int*char*string — это триплет, в котором первый элемент типа int, второй char, а третий — string.

( 65 , 'B' , "ascii" ) ;; - : int * char * string = 65, 'B', "ascii"


Warning
Если аргумент функций fst и snd не пара, а другой n–кортеж, то мы получим ошибку.

# snd ( 65 , 'B' , "ascii" ) ;; Characters 7-25: This expression has type int * char * string but is here used with type 'a * 'b

Существует разница между парой и триплетом: тип int*int*int отличен от (int*int)*int и int*(int*int). Методы доступа к элементам триплета (и других кортежей) не определены в стандартной библиотеке. В случае необходимости мы используем сопоставление с образцом (см. 1.2.1).

Списки

Значения одного и того же типа могут быть объединены в списки. Список может быть либо пустым, либо содержать однотипные элементы.

[] ;; - : 'a list = [] [ 1 ; 2 ; 3 ] ;; - : int list = [1; 2; 3] [ 1 ; "deux" ; 3 ] ;; Characters 15-18: This expression has type int list but is here used with type string list

Для того чтобы добавить элемент в начало списка существует следующая функция в виде инфиксного оператора :: — аналог cons в Caml.

1 :: 2 :: 3 :: [] ;; - : int list = [1; 2; 3]

Для объединения (конкатенации) списков существует инфиксный оператор: @.

[ 1 ] @ [ 2 ; 3 ] ;; - : int list = [1; 2; 3] [ 1 ; 2 ] @ [ 3 ] ;; - : int list = [1; 2; 3]

Остальные функции манипуляции списками определены в библиотеке List. Функции hd и tl дают доступ к первому и последнему элементу списка.

# List.hd [ 1 ; 2 ; 3 ] ;; - : int = 1 # List.hd [] ;; Uncaught exception: Failure("hd")

В последнем примере получить первый элемент пустого списка действительно “сложно”, поэтому возбуждается исключение (см. 1.3.1).

1.1.2  Структуры условного контроля

Одна из структур контроля необходимая в каждом языка программирования — условный оператор.

Синтаксис:

if expr1 then expr2 else expr3

Тип выражения expr1 равен bool. Выражения expr2 и expr3 должны быть одного и того же типа.

# if 3=4 then 0 else 4 ;; - : int = 4 # if 3=4 then "0" else "4" ;; - : string = "4" # if 3=4 then 0 else "4";; Characters 20-23: This expression has type string but is here used with type int

Условное выражение, как и любое другое, возвращает значение.

(if 3=5 then 8 else 10) + 5 ;; - : int = 15


Замечание
Ветка else может быть опущена, в этом случае будет подставлено значение по умолчанию равное else (), в соответствии с этим выражение expr2 должно быть типа unit (см. 2.3 стр. ??).

1.1.3  Объявление значений

Определение связывает имя со значением. Различают глобальные и локальные определения. В первом случае, объявленные имена видны во всех выражениях, следуют за ним, во втором — имена доступны только в текущем выражении. Мы также можем одновременно объявить несколько пар имя-значение.

Глобальные объявления

Синтаксис:

let nom = expr;

Глобальное объявление определяет связь имени nom со значением выражения expr, которое будет доступно всем следующим выражениям.

# let an = "1999" ;; val an : string = "1999" # let x = int_of_string(an) ;; val x : int = 1999 # x ;; - : int = 1999 # x + 1 ;; - : int = 2000 # let nouvel_an = string_of_int (x + 1) ;; val nouvel_an : string = "2000"

Одновременное глобальное объявление

Синтаксис:

let nom1 = expr1 and nom2 = expr2 : and nomn = exprn;;

При одновременном объявлении переменные будут известны только к концу всех объявлений.

# let x = 1 and y = 2 ;; val x : int = 1 val y : int = 2 # x + y ;; - : int = 3 # let z = 3 and t = z + 2 ;; Characters 18-19: Unbound value z

Можно сгруппировать несколько глобальных объявлений в одной фразе, вывод типов и значений произойдет к концу фразы, отмеченной “;;”. В данном случае объявления будут вычислены по порядку.

# let x = 2 let y = x + 3 ;; val x : int = 2 val y : int = 5

Глобальное объявление может быть скрыто локальным с тем же именем (см. 1.1.4 стр. ??).

Локальное объявление

Синтаксис:

let nom = expr1 in expr2};;

Имя nom связанное с выражением expr1 известно только для вычисления expr2.

# let xl = 3 in xl * xl ;; - : int = 9

Локальное объявление, которое связывает xl со значением 3, существует только в ходе вычисления xl*xl.

# xl ;; Characters 1-3: Unbound value xl

Локальное объявление скрывает любое глобальные с тем же именем, но как только мы выходим из блока в котором была оно определено, мы находим старое значение связанное с этим именем.

# let x = 2 ;; val x : int = 2 # let x = 3 in x * x ;; - : int = 9 # x * x ;; - : int = 4

Локальное объявление — это обычное выражение, соответственно оно может быть использовано для построения других выражений.

(let x = 3 in x * x) + 1 ;; - : int = 10

Локальные объявления так же могут быть одновременными.

let nom1 = expr1 and nom2 = expr2 : and nomn = exprn in expr ;;
# let a = 3.0 and b = 4.0 in sqrt (a*.a +. b*.b) ;; - : float = 5 # b ;; Characters 0-1: Unbound value b

1.1.4  Функциональное выражение, функции

Функциональное выражение состоит из параметра и тела. Формальный параметр — это имя переменной, а тело — это выражение. Обычно говорят что формальный параметр является абстрактным, по этой причине функциональное выражение тоже называется абстракцией.

Синтаксис:

function p -> expr

Таким образом функция возведения в квадрат будет выглядеть так:

# function x -> x*x ;; - : int -> int = <fun>

Objective CAML сам определяет тип. Функциональный тип int->int это функция с параметром типа int, возвращающая значение типа int.

Функция с одним аргументом пишется как функция и аргумент следующий за ней.

(function x -> x * x) 5 ;; - : int = 25

Вычисление функции состоит в вычисление ее тела, в данном случае x*x, где формальный параметр x, заменен значением аргумента (эффективным параметром), здесь он равен 5.

При конструкции функционального выражения expr может быть любым выражением, в частности функциональным.

# function x -> (function y -> 3*x + y) ;; - : int -> int -> int = <fun>

Скобки не обязательны, мы можем писать просто:

# function x -> function y -> 3*x + y ;; - : int -> int -> int = <fun>

В простом случае мы скажем, что функция ожидает два аргумента целого типа на входе и возвращает значение целого типа. Но когда речь идет о функциональном языке, таком как Objective CAML, то скорее это соответствует типу функции с входным аргументом типа int и возвращающая функциональное значение типа int->int:

(function x -> function y -> 3*x + y) 5 ;; - : int -> int = <fun>

Естественно, мы можем применить это функциональное выражение к двум аргументам. Для этого напишем:

(function x -> function y -> 3*x + y) 4 5 ;; - : int = 17

Когда мы пишем f a b, подразумевается применение (f a) к b.

Давайте подробно рассмотрим выражение

(function x -> function y -> 3*x + y) 4 5

Для того чтобы вычислить это выражение, необходимо сначала вычислить значение

(function x -> function y -> 3*x + y) 4

что есть функциональное выражение равное

function y -> 3*4 + y

в котором x заменен на 4 в выражении 3*x+y. Применяя это значение (являющееся функцией) к 5, мы получаем конечное значение 3*4+5=17:

(function x -> function y -> 3*x + y) 4 5 ;; - : int = 17

Арность функции

Арностью функции называется число аргументов функции. По правилам, унаследованным из математики, аргументы функции задаются в скобках после имени функции. Мы пишем: f(4,5). Как было указанно ранее, в Objective CAML мы чаще используем следующий синтаксис: f 4 5. Естественно, можно написать функциональное выражение применимое к (4,5):

# function (x,y) -> 3*x + y ;; - : int * int -> int = <fun>

Но в данном случае, функция ожидает не два аргумента, а один; тип которого пара целых. Попытка применить два аргумента к функции ожидающей пару или наоборот, передать пару функции для двух аргументов приведет к ошибке:

(function (x,y) -> 3*x + y) 4 5 ;; Characters 29-30: This expression has type int but is here used with type int * int (function x -> function y -> 3*x + y) (4, 5) ;; Characters 39-43: This expression has type int * int but is here used with type int

Альтернативный синтаксис

Существует более компактная форма записи функций с несколькими аргументами, которая дошла к нам из старых версий Caml. Выглядит она так:

Синтаксис

fun p1 ... pn -> expr

Это позволяет не повторять слово function и стрелки. Данная запись эквивалентна

function p1 -> -> function pn -> expr
# fun x y -> 3*x + y ;; - : int -> int -> int = <fun> (fun x y -> 3*x + y) 4 5 ;; - : int = 17

Эту форму можно часто встретить в библиотеках идущих в поставку с Objective CAML.

Замыкание

Objective CAML рассматривает функциональное выражение также как любое другое. Значение возвращаемое функциональным выражением называется замыканием. Каждое выражение Objective CAML вычисляется в окружении состоящем из соответствий имя–значение, которые были объявлены до вычисляемого выражения. Замыкание может быть описано как триплет, состоящий из имени формального параметра, тела функции и окружения выражения. Нам необходимо хранить это окружение, поскольку в теле функции кроме формальных параметров могут использоваться другие переменные. В функциональном выражении эти переменные называются свободными, нам понадобится их значение в момент применения функционального выражения.

# let m = 3 ;; val m : int = 3 # function x -> x + m ;; - : int -> int = <fun> (function x -> x + m) 5 ;; - : int = 8

В случае когда применение замыкания к аргументу возвращает новое замыкание, оно (новое замыкание) получает в свое окружение все необходимые связи для следующего применения. Раздел 1.1.4 подробно рассматривает эти понятия. В главе 3, а так же в главе 11 мы вернемся к тому как замыкание представляется в памяти.

До сих пор рассмотренные функциональные выражения были анонимными, однако мы можем дать им имя.

Объявление функциональных значений

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

# let succ = function x -> x + 1 ;; val succ : int -> int = <fun> # succ 420 ;; - : int = 421 # let g = function x -> function y -> 2*x + 3*y ;; val g : int -> int -> int = <fun> # g 1 2;; - : int = 8

Для упрощения записи, можно использовать следующий синтаксис:

Синтаксис:

let nom p1...pn=expr

что эквивалентно:

let nom=function p1->->function pn-> expr

Следующие объявления succ и g эквивалентны предыдущим:

# let succ x = x + 1 ;; val succ : int -> int = <fun> # let g x y = 2*x + 3*y ;; val g : int -> int -> int = <fun>

В следующем примере демонстрируется функциональная сторона Objective CAML, где функция h1 получена применением g к аргументу. В данном случае мы имеем дело с частичным применением.

# let h1 = g 1 ;; val h1 : int -> int = <fun> # h1 2 ;; - : int = 8

С помощью g мы можем определить другую функцию h2 фиксируя значение второго параметра (y):

# let h2 = function x -> g x 2 ;; val h2 : int -> int = <fun> # h2 1 ;; - : int = 8

Объявление инфиксных функций

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

В следующем примере определяется функция succ используя (+):

(+) ;; - : int -> int -> int = <fun> # let succ = (+) 1 ;; val succ : int -> int = <fun> # succ 3 ;; - : int = 4

Таким образом мы можем определить новые операторы, что мы и сделаем определив ++ для сложения двух пар целых.

# let (++) c1 c2 = (fst c1)+(fst c2), (snd c1)+(snd c2) ;; val ++ : int * int -> int * int -> int * int = <fun> # let c = (2,3) ;; val c : int * int = 2, 3 # c ++ c ;; - : int * int = 4, 6

Существуют однако ограничения на определение новых операторов, они должны содержать только символы (такие как *,+,$, etc.), исключая буквы и цифры. Следующие функции являются исключением:

or mod land lor lxor lsr asr

Функции высшего порядка

Функциональное значение (замыкание) может быть возвращено как результат, а так же передано как аргумент функции. Такие функции, берущие на входе или возвращающие функциональные значения, называются функциями высшего порядка.

# let h = function f -> function y -> (f y) + y ;; val h : (int -> int) -> int -> int = <fun>


Замечание
Выражения группируются справа налево, но функциональные типы объединяются слева направо. Таким образом тип функции h может быть написан:

(int -> int) -> int -> int или (int -> int) -> (int -> int)

При помощи функций высшего порядка можно элегантно обрабатывать списки. К примеру функция List.map применяет какую-нибудь функцию ко всем элементам списка и возвращает список результатов.

# List.map ;; - : ('a -> 'b) -> 'a list -> 'b list = <fun> # let square x = string_of_int (x*x) ;; val square : int -> string = <fun> # List.map square [1; 2; 3; 4] ;; - : string list = ["1"; "4"; "9"; "16"]

Другой пример — функция List.for_all проверяет соответствуют ли элементы списка определенному критерию.

# List.for_all ;; - : ('a -> bool) -> 'a list -> bool = <fun> # List.for_all (function n -> n<>0) [-3; -2; -1; 1; 2; 3] ;; - : bool = true # List.for_all (function n -> n<>0) [-3; -2; 0; 1; 2; 3] ;; - : bool = false

Видимость переменных

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

let p=e

Переменная p не известна в этом выражении, она может быть использована только в случае если p была объявлена ранее.

# let p = p ^ "-suffixe" ;; Characters 9-10: Unbound value p # let p = "préfixe" ;; val p : string = "pr\233fixe" # let p = p ^ "-suffixe" ;; val p : string = "pr\233fixe-suffixe"

В Objective CAML переменные связаны статически. При применении замыкания используется окружение в момент ее (замыкания) объявления (статическая видимость), а не в момент ее применения (динамическая видимость)

# let p = 10 ;; val p : int = 10 # let k x = (x, p, x+p) ;; val k : int -> int * int * int = <fun> # k p ;; - : int * int * int = 10, 10, 20 # let p = 1000 ;; val p : int = 1000 # k p ;; - : int * int * int = 1000, 10, 1010

В функции k имеется свободная переменная p, которая была определена в глобальном окружении, поэтому определение k принято. Связь между именем p и значением 10 в окружении замыкания k статическая, то есть не зависит от последнего определения p.

Рекурсивное объявление

Объявление переменной называется рекурсивным, если оно использует свой собственный идентификатор в своем определении. Эта возможность часто используется для определения рекурсивных функций. Как мы видели ранее, let не позволяет делать это, поэтому необходимо использовать специальный синтаксис:

let rec nom = expr ;;

Другой способ записи для функции с аргументами:

let rec nom p1 ... pn = expr ;;

Определим функцию sigma вычисляющую сумму целых от 0 до значения указанного аргументом:

# let rec sigma x = if x = 0 then 0 else x + sigma (x-1) ;; val sigma : int -> int = <fun> # sigma 10 ;; - : int = 55

Как заметил читатель, эта функция рискует “не закончиться” если входной аргумент меньше 0.

Обычно, рекурсивное значение — это функция, компилятор не принимает некоторые рекурсивные объявления, значения которых не функциональные.

# let rec x = x + 1 ;; Characters 13-18: This kind of expression is not allowed as right-hand side of `let rec'

Как мы увидим позднее, такие определения все таки возможны в некоторых случаях (см. 1.2.10 стр. ??).

Объявление let rec может быть скомбинировано с and. В этом случае функции определенные на одном и том же уровне, видны всем остальным. Это может быть полезно при декларации взаимно рекурсивных функций.

# let rec pair n = (n<>1) && ((n=0) or (impair (n-1))) and impair n = (n<>0) && ((n=1) or (pair (n-1)));; val pair : int -> bool = <fun> val impair : int -> bool = <fun> # pair 4 ;; - : bool = true # impair 5 ;; - : bool = true

По тому же принципу, локальные функции могут быть рекурсивными. Новое определение функции sigma проверяет корректность входного аргумента, перед тем как посчитать сумму локальной функцией sigma_rec.

# let sigma x = let rec sigma_rec x = if x = 0 then 0 else x + sigma_rec (x-1) in if (x<0) then "erreur : argument negatif" else "sigma = " ^ (string_of_int (sigma_rec x)) ;; val sigma : int -> string = <fun>


Замечание
Мы вынужденны были определить возвращаемый тип как string, поскольку необходимо чтобы он был один и тот же, независимо от входного аргумента, отрицательного или положительного. Какое значение должна вернуть sigma если аргумент больше нуля? Далее, мы увидим правильный способ решения этой проблемы (см. 1.3 стр. ??).

1.1.5  Полиморфизм и ограничение типа

Некоторые функции выполняют одни и те же инструкции независимо от типа аргументов. К примеру, для создание пары из двух значений нет смысла определять функции для каждого известного типа. Другой пример, доступ к первому полю пары не зависит от того, какого типа это поле.

# let make_pair a b = (a,b) ;; val make_pair : 'a -> 'b -> 'a * 'b = <fun> # let p = make_pair "papier" 451 ;; val p : string * int = "papier", 451 # let a = make_pair 'B' 65 ;; val a : char * int = 'B', 65 # fst p ;; - : string = "papier" # fst a ;; - : char = 'B'

Функция, для которой не нужно указывать тип входного аргумента или возвращаемого значения называется полиморфной. Синтезатор типов, включенный в компилятор Objective CAML находит наиболее общий тип для каждого выражения. В этом случае Objective CAML использует переменные, здесь они обозначены как 'a и 'b, для указания общих типов. Эти переменные конкретизируются типом аргумента в момент применения функции.

При помощи полиморфных функций, мы получаем возможность написания универсального кода для любого типа переменных, сохраняя при этом надежность статической типизации. Действительно, несмотря на то что make_paire полиморфная, значение созданное (make_paire '6' 65) имеет строго определенный тип, который отличен от (make_paire "paire" 451). Проверка типов реализуется в момент компиляции, таким образом универсальность кода никак не сказывается на эффективности программы.

Пример функций и полиморфных значений

В следующем примере приведена полиморфная функция с входным параметром функционального типа.

# let app = function f -> function x -> f x ;; val app : ('a -> 'b) -> 'a -> 'b = <fun>

Мы можем применить ее к функции impaire, которая была определена ранее.

# app impair 2;; - : bool = false

Функция тождества (id) возвращает полученный аргумент.

# let id x = x ;; val id : 'a -> 'a = <fun> # app id 1 ;; - : int = 1

Следующая функция, compose принимает на входе две функции и еще один аргумент, к которому применяет две первые.

# let compose f g x = f (g x) ;; val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun> # let add1 x = x+1 and mul5 x = x*5 in compose mul5 add1 9 ;; - : int = 50

Как мы видим, тип результата возвращаемого g должен быть таким же как и тип входного аргумента f.

Не только функциональные значения могут быть полиморфными, проиллюстрируем это на примере пустого списка.

# let l = [] ;; val l : 'a list = []

Следующий пример иллюстрирует тот факт, что создание типов основывается на разрешении ограничений, накладываемых применением функций, а вовсе не на значениях, полученных в процессе выполнения.

# let q = List.tl [2] ;; val q : int list = []

Здесь тип равен List.tl 'a list->'a list, то есть эта функция применяется к списку целых и возвращает список целых. Тот факт что в момент выполнения возвращен пустой список, не влияет на его тип.

Objective CAML создает параметризованные типы для каждой функции, которые не зависят от ее аргументов. Этот вид полиморфизма называется параметрическим полиморфизмом.2

Ограничение типа

Синтезатор типа Objective CAML образует самый общий тип и иногда бывает необходимо уточнить тип выражения.

Синтаксис ограничения типа следующий:

(expr:t)

В этом случае синтезатор типа воспользуется этим ограничением при конструкции типа выражения. Использование ограничения типа позволяет

Рассмотрим использование ограничения типа.

# let add (x:int) (y:int) = x + y ;; val add : int -> int -> int = <fun> # let make_pair_int (x:int) (y:int) = x,y;; val make_pair_int : int -> int -> int * int = <fun> # let compose_fn_int (f : int -> int) (g : int -> int) (x:int) = compose f g x;; val compose_fn_int : (int -> int) -> (int -> int) -> int -> int = <fun> # let nil = ([] : string list);; val nil : string list = [] 'H'::nil;; Characters 5-8: This expression has type string list but is here used with type char list

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

# let llnil = ([] : 'a list list) ;; val llnil : 'a list list = [] [1;2;3]:: llnil ;; - : int list list = [[1; 2; 3]]

llint является списком списков любого типа.

В данном случае подразумевается ограничение типа, а не явная типизация заменяющая тип, который определил Objective CAML. В частности, мы не можем обобщить тип, более чем это позволяет вывод типов Objective CAML.

# let add_general (x:'a) (y:'b) = add x y ;; val add_general : int -> int -> int = <fun>

Ограничения типа будут использованы в интерфейсах модулей 13, а так же в декларации классов 14

1.1.6  Примеры

В этом параграфе мы приведем несколько примеров функций. Большинство из них уже определены в Objective CAML, мы делаем это только в “педагогических” целях.

Тест остановки рекурсивных функций реализован при помощи проверки, имеющей стиль более близкий к Lisp. Мы увидим как это сделать в стиле ML (см. 1.2.1 стр. ??).

Размер списка

Начнем с функции проверяющей пустой список или нет.

# let null l = (l=[]);; val null : 'a list -> bool = <fun>

Определим функцию size вычисления размера списка (т.е. число элементов).

# let rec size l = if null l then 0 else 1+(size(List.tl l));; val size : 'a list -> int = <fun> # size [] ;; - : int = 0 # size [1;2;18;22] ;; - : int = 4

Функция size проверяет список: если он пуст возвращает 0, иначе прибавляет 1 к длине остатка списка.

Итерация композиций (Iteration of composition)

Выражение iterate n f вычисляет fn, соответствующее применение функции f n раз.

# let ret iterate n f = if n=0 then (function x->x) else compose f (iterate (n-1) f);; val iterate : int -> ('a -> 'a) -> 'a -> 'a = <fun>

Функция iterate проверяет аргумент n на равенство нулю, если аргумент равен, то возвращаем функцию идентичности, иначе возвращаем композицию f с итерацией f n-1 раз.

Используя iterate можно определить операцию возведения в степень, как итерацию умножения.

# let power i n =
let i_times = (*) i in
iterate n i_times 1;;
val power : int -> int -> int = <fun>
# power 2 8 ;;
- : int = 256

Функция power повторяет n раз функциональное выражение i_times, затем применяет этот результат к 1, таким образом мы получаем n-ю степень целого числа.

Таблица умножения

Напишем функцию multab, которая вычисляет ряд таблицы умножения соответствующую целому числу переданному в аргументе.

Для начала определим функцию apply_fun_list. Пусть f_list список функций, тогда вызов apply_fun_list x f_list возвращает список результатов применения каждого элемента списка f_list к x.

# let rec apply_fun_list x f_list = if null f_list then [] else ((List.hd f_list) x)::(apply_fun_list x (List.tl f_list)) ;; val apply_fun_list : 'a -> ('a -> 'b) list -> 'b list = <fun> # apply_fun_list 1 [(+) 1;(+) 2;(+) 3] ;; - : int list = [2; 3; 4]

Функция mk_mult_fun_list возвращает список функций умножающих их аргумент на i, 0<=i<=n.

# let mk_mult_fun_list n = let rec mmfl_aux p = if p = n then [ ( * ) n ] else (( * ) p) :: (mmfl_aux (p+1)) in (mmfl_aux 1) ;; val mk_mult_fun_list : int -> (int -> int) list = <fun>

Подсчитаем ряд для 7

# let multab n = apply_fun_list n (mk_mult_fun_list 10) ;; val multab : int -> int list = <fun> # multab 7 ;; - : int list = [7; 14; 21; 28; 35; 42; 49; 56; 63; 70]

Итерация в списке

Вызов функции fold_left f a[e1;e2;…;en] возвращает f…(f (f a e1) e2) …en, значит получаем n применений.

# let rec fold_left f a l = if null l then a else fold_left f ( f a (List.hd l)) (List.tl l) ;; val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>

С помощью функции fold_left можно компактно определить функцию вычисления суммы элементов списка целых чисел.

# let sum_list = fold_left (+) 0 ;; val sum_list : int list -> int = <fun> # sum_list [2;4;7] ;; - : int = 13

Или, например конкатенация элементов списка строк.

# let concat_list = fold_left (^) "" ;; val concat_list : string list -> string = <fun> # concat_list ["Hello "; "world" ; " "; "!"] ;; - : string = "Hello world !"

1.2  Объявления типов и сопоставление с образцом

С помощью типов определеных в Objective CAML мы можем определять структурные типы, при помощи кортежей или списков. Но в некоторых случаях бывает необходимо определить новые типы для описания специальных структур. В Objective CAML, объявления типов рекурсивны и могут быть параметризованы переменными типа, как в случае 'a list который мы уже обсуждали. Вывод типов принимает во внимание новые объявления для определения типов выражений. Конструкция значений новых типов использует конструктор описанный определением типа. Особой возможностью языков семейства ML является сопоставление с образцом, которое обеспечивает простой метод доступа к компонентам (полям) структур данных. Определение функции соответствует чаще всего сопоставлению с образцом по одному из ее параметров, что позволяет определять функции для различных случаев.

Для начала, продемонстрируем механизм сопоставления на существующих типах и затем опишем различные объявления структурных типов, конструкций таких переменных и доступ к компонентам по сопоставлению с образцом.

1.2.1  Сопоставление с образцом

Образец (по английски pattern) строго говоря не совсем выражение Objective CAML. Речь скорее идет о корректной компоновке (синтаксической и с точки зрения типов) элементов, таких как константы базовых типов (int, bool, char…), переменные, конструкторы и символ _, называемый универсальным образцом. Другие символы служат для записи шаблона, которые мы опишем по ходу.

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

Синтаксис:

match expr with | p1 -> expr1 : | pn -> exprn

Выражение expr последовательно сопоставляется (фильтруется) с разными образцами p1…,pn. Если один из образцов (например pi) соответствует значению expr, то соответствующее ответвление будет вычислено (expri). Образцы pi одинакового типа, так же как и expri. Вертикальная черта перед первым образцом не обязательна.

Пример

Приведем два способа, при помощи сопоставления с образцом, определения функции imply типа (bool * bool)->bool, реализующую логическую импликацию. Образец для пар — (,).

Первая версия перечисляет все возможности, как таблица истинности.

# let imply v = match v with (true,true) -> true | (true,false) -> false | (false,true) -> true | (false,false) -> true;; val imply : bool * bool -> bool = <fun>

Используя переменные группирующие несколько случаев, мы получаем более компактное определение.

# let imply v = match v with (true,x) -> x | (false,x) -> true;; val imply : bool * bool -> bool = <fun>

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

Линейный образец

Образец обязательно должен быть линейным, то есть определенная переменная не может быть использована дважды. Мы могли бы написать:

# let equal c = match c with (x,x) -> true | (x,y) -> false;; Characters 35-36: This variable is bound several times in this matching

Но для этого компилятор должен уметь реализовывать тесты на равенство, что приводит к множеству проблем. Если мы ограничимся физическим значением переменных, то мы получим слишком “слабую” систему, не в состоянии, например, распознать равенство между двумя списками [1; 2]. Если же мы будет проверять на структурное равенство, то рискуем бесконечно проверять циклические структуры. Рекурсивные функции, например, это циклические структуры, но мы так же можем определить рекурсивные значения не являющиеся функциями (см. 1.2.10 стр. ??).

Универсальный образец

Символ _ совпадает со всеми возможными значениями — он называется универсальным. Этот образец может быть использован для сопоставления сложных типов. Воспользуемся им для определения еще одной версии функции imply:

# let imply v = match v with (true,false) -> false | _ -> true;; val imply : bool * bool -> bool = <fun>

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

# let is_zero n = match n with 0 -> true ;; Characters 17-40: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: 1 val is_zero : int -> bool = <fun>

В случае если аргумент не равен 0, функция не знает какое значение должно быть возвращено. Для исправления добавим универсальный образец:

# let is_zero n = match n with 0 -> true | _ -> false ;; val is_zero : int -> bool = <fun>

Если во время выполнения ни один образец не выбран, возбуждается исключение:

# let f x = match x with 1 -> 3 ;; Characters 11-30: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: 0 val f : int -> int = <fun> # f 1 ;; - : int = 3 # f 4 ;; Uncaught exception: Match_failure("", 11, 30)

Исключение Match_Failure возбуждено при вызове f 4 и если оно не обработано, то вычисление останавливается (см. 1.3 стр. ??).

Комбинация образцов

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

Синтаксис

p1 | ...| pn

Эта форма создает новый образец из комбинации мотивов p1,…,pn, с единственным ограничением — каждый из этих образцов должен содержать константные значения либо универсальный образец.

Следующий пример показывает как проверить является ли входной символ гласной буквой.

# let est_une_voyelle c = match c with 'a' | 'e' | 'i' | 'o' | 'u' | 'y' -> true | _ -> false ;; val est_une_voyelle : char -> bool = <fun> # est_une_voyelle 'i' ;; - : bool = true # est_une_voyelle 'j' ;; - : bool = false

Параметризованное сопоставление

Параметризованное сопоставление используется в основном для определения функций выбора. Для облегчения записи подобных определений, конструктор function разрешает следующее сопоставление одного параметра.

Синтаксис

function | p1 -> expr1 | p_2 -> expr2 : | p_n -> exprn

Вертикальная черта перед первым образцом не обязательна. Каждый раз при определении функции, мы используем сопоставление с образцом. Действительно, конструкция function x -> expression является определением сопоставления по уникальному образцу одной переменной. Можно использовать эту особенность в простых шаблонах:

# let f = function (x,y) -> 2*x + 3*y + 4 ;; val f : int * int -> int = <fun>

Форма

function p1 -> expr1 | ...| pn -> exprn

эквивалентна

function expr -> match expr with p1 -> expr1 | ...| pn -> exprn

Используя схожесть определения на (см. 1.1.4 стр. ??), мы бы написали:

# let f (x,y) = 2*x + 3*y + 4 ;; val f : int * int -> int = <fun>

Но подобная запись возможна лишь в случае если фильтруемое значение принадлежит к типу с единственным конструктором, иначе сопоставление не является исчерпывающим:

# let is_zero 0 = true ;; Characters 13-21: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: 1 val is_zero : int -> bool = <fun>

Присвоение имени фильтруемому значению

Иногда при сопоставлении с образцом, бывает необходимо дать имя всему образцу или лишь его части. Следующая синтаксическая форма вводит ключевое слово as, которое ассоциирует имя образцу.

Синтаксис

(p as nom)

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

# let min_rat cr = match cr with ((_,0),c2) -> c2 |(c1,(_,0)) -> c1 |(((n1,d1) as r1), ((n2,d2) as r2)) -> if (n1 * d2 ) < (n2 * d1) then r1 else r2;; val min_rat : (int * int) * (int * int) -> int * int = <fun>

Для сравнения двух рациональных чисел, необходимо разбить их для именования числителя и знаменателя (n1, n2, d1 и d2), но так же для собирания в одно целое начальные пары (r1 или r2). Конструкция позволяет дать имена частям значения — это избавляет от реконструкции рационального числа при возвращении результата.

Сопоставление с ограничением

Сопоставление с ограничением соответствует вычислению условного выражения сразу после сопоставления с образцом. Если это выражение возвращает значение true, то выражение соответствующее этому образцу будет вычислено, иначе сопоставление будет продолжаться дальше.

Синтаксис:

match expr with : | pi when condi -> expri :

В следующем примере используется два ограничения для проверки равенства двух рациональных чисел.

# let eq_rat cr = match cr with ((_,0),(_,0)) -> true | ((_,0),_) -> false | (_,(_,0)) -> false | ((n1,1), (n2,1)) when n1 = n2 -> true | ((n1,d1), (n2,d2)) when ((n1 * d2) = (n2 * d1)) -> true | _ -> false;; val eq_rat : (int * int) * (int * int) -> bool = <fun>

Если при фильтрации четвертого образца ограничение не выполнится, то сопоставление продолжится по пятому образцу.


Warning
При проверке исчерпываемости сопоставления Objective CAML предполагает что условное выражение может быть ложным. В следствии, верификатор не учитывает этот образец, так как не возможно знать до выполнения сработает ограничение или нет. Исчерпываемость следующего фильтра не может быть определена.

# let f = function x when x = x -> true;; Characters 10-40: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: _ val f : 'a -> bool = <fun>

Образец интервала символов

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

Синтаксис

'c1' .. 'cn'

что соответствует 'c1' | 'c2' | ...| 'cn'

К примеру образец

'0' .. '9'

соответствует образу

'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'.

Первая форма быстрее и проще воспринимается при чтении.


Warning
Это особенность является расширением языка и может измениться в следующих версиях.

Используя комбинированные образцы и интервалы, напишем функцию по определению символа по нескольким критериям.

# let char_discriminate c = match c with 'a' | 'e' | 'i' | 'o' | 'u' | 'y' | 'A' | 'E' | 'I' | 'O' | 'U' | 'Y' -> "Voyelle" | 'a'..'z' | 'A'..'Z' -> "Consonne" | '0'..'9' -> "Chiffre" | _ -> "Autre" ;; val char_discriminate : char -> string = <fun>

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

Образцы списков

Как мы уже видели список может быть:

Обе эти записи могут быть использованы в образце при фильтрации списка.

# let rec size x = match x with [] -> 0 | _::queue_x -> 1 + (size queue_x) ;; val size : 'a list -> int = <fun> # size [];; - : int = 0 # size [7;9;2;6];; - : int = 4

Перепишем раннее показанный пример (см. 1.1.6 стр. ??) используя сопоставление с образом для итерации списков.

# let rec fold_left f a = function [] -> a | tete::queue -> fold_left f (f a tete) queue ;; val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun> # fold_left (+) 0 [8;4;10];; - : int = 22

Объявление значения через сопоставлением с образцом

Объявление значений само по себе использует сопоставление. let x=18 сопоставляет образцы x со значением 18. Любой образец принимается как фильтр объявления, переменные образца ассоциируются со значениями которые они сопоставляют.

# let (a,b,c) = (1, true, 'A');; val a : int = 1 val b : bool = true val c : char = 'A' # let (d,c) = 8, 3 in d + c;; - : int = 11

Видимость переменных фильтра та же самая что у локальных переменных. Здесь c ассоциировано с 'A'.

# a + (int_of_char c);; - : int = 66

Как и любой фильтр, определение значения может быть не исчерпывающим.

# let [x;y;z] = [1;2;3];; Characters 5-12: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: [] val x : int = 1 val y : int = 2 val z : int = 3 # let [x;y;z] = [1;2;3;4];; Characters 4-11: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: [] Uncaught exception: Match_failure("", 4, 11)

Принимается любой образец, включая конструктор, универсальный и комбинированный.

# let tete :: 2 :: _ = [1; 2; 3] ;; Characters 5-19: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: [] val tete : int = 1 # let _ = 3. +. 0.14 in "PI" ;; - : string = "PI"

Последний пример мало интересен для функционального программирования, поскольку значение 3.14 не именовано, и соответственно будет потерянно.

1.2.2  Декларация типов

Объявление типа — это одна из других возможных фраз синтаксиса Objective CAML. Она позволяет определить новые типы соответствующие обычным структурам данных используемым в программах. Существует два больших семейства типов: тип–произведение для кортежей или записей или тип–сумма для union.

Синтаксис:

type nom = typedef ;;

В отличии от объявления переменных, объявление типов по умолчанию рекурсивно. То есть объявления типов, когда они скомбинированы, могут объявлять взаимно рекурсивные типы.

Синтаксис:

type nom1 = typedef1 and nom2 = typedef2 : and nomn = typedefn ;;

Объявления типов могут быть параметризованы переменной типа. Имя переменной типа начинается всегда с апострофа (символ ').

Синтаксис

type 'a nom = typedef ;;

В случае когда таких переменных несколько, параметры типа декларируются как кортеж перед именем типа.

Синтаксис

type ('a1 ...'an) nom = typedef ;;

Только те параметры, которые определены в левой части декларации, могут появится в ее правой части.


Замечание
Во время вывода на экран Objective CAML переименует параметры типа, первый в 'a, второй в 'b и так далее.
Мы всегда можем объявить новый тип используя ранее объявленные типы.

Синтаксис:

type name = type expression

Это полезно в том случае, когда мы хотим ограничить слишком общий тип:

# type 'param couple_entier = int * 'param ;; type 'a couple_entier = int * 'a # type couple_precis = float couple_entier ;; type couple_precis = float couple_entier

Без ограничения, выводится наиболее общий тип:

# let x = (3, 3.14) ;; val x : int * float = 3, 3.14

Но мы можем использовать ограничитель типа для того, чтобы получить желаемое имя:

# let (x:couple_precis) = (3, 3.14) ;; val x : couple_precis = 3, 3.14

1.2.3  Записи

Запись — это кортеж, в котором каждому полю присваивается имя на подобии record в Pascal или struct в C. Запись всегда соответствует объявлению нового типа. Для определения записи необходимо указать ее имя, а так же имя и тип для каждого поля записи.

Синтаксис:

type nom = { nom1 : t1; ...; nomn : tn } ;;

Определим тип комплексного числа следующим образом.

# type complex = { re:float; im:float } ;; type complex = { re: float; im: float }

Для того чтобы создать значение типа запись, нужно присвоить каждому полю значение (в каком угодно порядке).

Синтаксис:

{ nomi1 = expri1; ...; nomin = exprin } ;;

В следующем примере мы создадим комплексное число, в котором реальная часть равна 2 и мнимая 3:

# let c = {re=2.;im=3.} ;; val c : complex = {re=2; im=3} # c = {im=3.;re=2.} ;; - : bool = true

В случае если не хватает некоторых полей, произойдет следующая ошибка:

# let d = { im=4. } ;; Characters 9-18: Some labels are undefined

Доступ к полям возможен двумя способами: синтаксис с точкой, либо сопоставлением некоторых полей.

Синтаксис с точкой заключается в следующем:

Синтаксис:

expr.nom

Выражение expr должно иметь тип “запись с полем nom”.

Сопоставление записи позволяет получить значение связанное с определенным полям. Синтаксис образца для записи следующий.

Синтаксис

{ nomi = pi ; ...; nomj = pj }

Образцы находятся справа от символа = (pi, ..., pj). Необязательно перечислять все поля записи в образце.

Функция add_complex обращается к полям с помощью “точки”, тогда как функция mult_complex обращается при помощи фильтра.

# let add_complex c1 c2 = {re=c1.re+.c2.re; im=c1.im+.c2.im};; val add_complex : complex -> complex -> complex = <fun> # add_complex c c ;; - : complex = {re=4; im=6} # let mult_complex c1 c2 = match (c1,c2) with ({re=x1;im=y1},{re=x2;im=y2}) -> {re=x1*.x2-.y1*.y2;im=x1*.y2+.x2*.y1} ;; val mult_complex : complex -> complex -> complex = <fun> # mult_complex c c ;; - : complex = {re=-5; im=12}

Преимущество записей, по сравнению с кортежами как минимум двойное:

# let a = (1,2,3) ;; val a : int * int * int = 1, 2, 3 # let f tr = match tr with x,_,_ -> x ;; val f : 'a * 'b * 'c -> 'a = <fun> # f a ;; - : int = 1 # type triplet = {x1:int; x2:int; x3:int} ;; type triplet = { x1: int; x2: int; x3: int } # let b = {x1=1; x2=2; x3=3} ;; val b : triplet = {x1=1; x2=2; x3=3} # let g tr = tr.x1 ;; val g : triplet -> int = <fun> # g b ;; - : int = 1

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

# let h tr = match tr with {x1=x} -> x;; val h : triplet -> int = <fun> # h b;; - : int = 1

Существует конструкция позволяющая создать идентичную запись, с разницей в несколько полей, что удобно для записей с большим числом полей.

Синтаксис:

{ name with namei= expri ; ...; namej=expr_j}
# let c = {b with x1=0} ;; val c : triplet = {x1=0; x2=2; x3=3}

Новая копия значения b отличается только лишь значением поля x1.


Warning
Это особенность является расширением языка и может измениться в следующих версиях.

1.2.4  Тип сумма (sum)

В отличии от кортежей или записей, которые соответствуют декартовому произведению, тип сумма соответствует объединению множеств (union). В один тип мы группируем несколько разных типов (например, целые числа и строки). Различные члены суммы определяются конструкторами, которые с одной стороны позволяют сконструировать значение этого типа и с другой получить доступ к компонентам этих значений при помощи сопоставления с образцом. Применить конструктор к аргументу, значит указать что возвращаемое значение будет этого нового типа.

Тип сумма описывается указыванием имени конструкторов и типа их возможного аргумента.

Синтаксис:

type nom = ... | Nomi ... | Nomj of tj ... | Nomk of tk * ...* tl ...;;

Имя конструктора это специальный идентификатор.


Замечание
Имя конструктора должно всегда начинаться с заглавной буквы.

Константный конструктор

Мы называем константным, конструктор без аргументов. Такие конструктора могут затем использоваться как значения языка, в качестве констант.

(Орел Решка)

# type piece = Pile | Face;; type piece = | Pile | Face # Pile;; - : piece = Pile

Тип может быть определен подобным образом.

Конструктор с аргументами

Конструктора могут иметь аргументы, ключевое слово of указывает тип аргумента. Это позволяет сгруппировать под одним типом объекты разных типов, имеющих разные конструктора. Определим типы couleur и carte следующим образом.

N.B.

# type couleur = Pique | Coeur | Carreau | Trefle ;; # type carte = Roi of couleur | Dame of couleur | Cavalier of couleur | Valet of couleur | Petite_carte of couleur * int | Atout of int | Excuse ;;

Создание значения типа carte получается применением конструктора к значению с нужным типом.

# Roi Pique ;; - : carte = Roi Pique # Petite_carte(Coeur, 10) ;; - : carte = Petite_carte (Coeur, 10) # Atout 21 ;; - : carte = Atout 21

Следующая функция, toutes_les_cartes, создает список всех карт цвета указанного аргументом.

# let rec interval a b = if a = b then [b] else a::(interval (a+1) b) ;; val interval : int -> int -> int list = <fun> # let toutes_les_cartes s = let les_figures = [ Valet s; Cavalier s; Dame s; Roi s ] and les_autres = List.map (function n -> Petite_carte(s,n)) (interval 1 10) in les_figures @ les_autres ;; val toutes_les_cartes : couleur -> carte list = <fun> # toutes_les_cartes Coeur ;; - : carte list = [Valet Coeur; Cavalier Coeur; Dame Coeur; Roi Coeur; Petite_carte (Coeur, 1); Petite_carte (Coeur, 2); Petite_carte (Coeur, 3); Petite_carte (Coeur, ...); ...]

Для манипуляции значений типа сумма, мы используем сопоставление с образцом. В следующем примере опишем функцию преобразующую значения типа couleur и типа carte в строку (тип string):

# let string_of_couleur = function Pique -> "pique" | Carreau -> "carreau" | Coeur -> "coeur" | Trefle -> "trefle" ;; val string_of_couleur : couleur -> string = <fun> # let string_of_carte = function Roi c -> "roi de " ^ (string_of_couleur c) | Dame c -> "dame de " ^ (string_of_couleur c) | Valet c -> "valet de " ^ (string_of_couleur c) | Cavalier c -> "cavalier de " ^ (string_of_couleur c) | Petite_carte (c, n) -> (string_of_int n) ^ " de "^(string_of_couleur c) | Atout n -> (string_of_int n) ^ " d'atout" | Excuse -> "excuse" ;; val string_of_carte : carte -> string = <fun>

Конструктор Petite_carte бинарный, для сопоставления такого значения мы должны дать имя обоим компонентам.

# let est_petite_carte c = match c with Petite_carte v -> true | _ -> false;; %Characters 45-59:% %The constructor Petite_carte expects 2 argument(s),% %but is here applied to 1 argument(s)%

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

# type t = C of int * bool | D of (int * bool) ;; # let acces v = match v with C (i, b) -> i,b | D x -> x;; val acces : t -> int * bool = <fun>

1.2.5  Рекурсивный тип

Определение рекурсивного типа необходимо в любом языке программирования, с его помощью мы можем определить такие типы как список, стек, дерево и так далее. В отличии от объявления let, type всегда рекурсивный в Objective CAML.

Типу список, определенному в Objective CAML, необходим один единственный аргумент. Для того чтобы хранить значения двух разных типов, как например int или char мы напишем:

# type int_or_char_list = Nil | Int_cons of int * int_or_char_list | Char_cons of char * int_or_char_list ;; # let l1 = Char_cons ( '=', Int_cons(5, Nil) ) in Int_cons ( 2, Char_cons ( '+', Int_cons(3, l1) ) ) ;; - : int_or_char_list = Int_cons (2, Char_cons ('+', Int_cons (3, Char_cons ('=', Int_cons (...)))))

1.2.6  Параметризованный тип

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

# type ('a, 'b) list2 = Nil | Acons of 'a * ('a, 'b) list2 | Bcons of 'b * ('a, 'b) list2 ;; # Acons(2, Bcons('+', Acons(3, Bcons('=', Acons(5, Nil))))) ;; - : (int, char) list2 = Acons (2, Bcons ('+', Acons (3, Bcons ('=', Acons (...)))))

Естественно, оба параметра 'a и 'b могут быть одного типа:

# Acons(1, Bcons(2, Acons(3, Bcons(4, Nil)))) ;; - : (int, int) list2 = Acons (1, Bcons (2, Acons (3, Bcons (4, Nil))))

Как в предыдущем примере, мы можем использовать тип list2 чтобы пометить четные и нечетные числа. Что бы создать обычный список, достаточно извлечь под–список четных чисел.

# let rec extract_odd = function Nil -> [] | Acons(_, x) -> extract_odd x | Bcons(n, x) -> n::(extract_odd x) ;; val extract_odd : ('a, 'b) list2 -> 'b list = <fun>

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

1.2.7  Видимость описания

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

В следующем примере, константный конструктор Nil типа int_of_char скрывается объявлением конструктора ('a, 'b) list2.

# Int_cons(0, Nil) ;; Characters 13-16: This expression has type ('a, 'b) list2 but is here used with type int_or_char_list

Во втором примере мы получим совсем бестолковое сообщение, по крайней мере на первый взгляд. Пусть мы имеем следующую программу:

# type t1 = Vide | Plein;; type t1 = | Vide | Plein # let vide_t1 x = match x with Vide -> true | Plein -> false ;; val vide_t1 : t1 -> bool = <fun> # vide_t1 Vide;; - : bool = true

Затем переопределим тип t1:

# type t1 = {u : int; v : int} ;; type t1 = { u: int; v: int } # let y = { u=2; v=3 } ;; val y : t1 = {u=2; v=3}

Теперь если мы применим функцию empty_t1 к значению с новым типом t1, то получим следующее сообщение.

# empty_t1 y;; Characters 9-10: This expression has type t1 but is here used with type t1

Первое упоминание типа t1 соответствует ранее определенному типу, тогда как второе — последнему.

1.2.8  Функциональные типы

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

# type 'a listf = Val of 'a | Fun of ('a -> 'a) * 'a listf ;; type 'a listf = | Val of 'a | Fun of ('a -> 'a) * 'a listf

Поскольку функциональные значения входят во множество значений которые обрабатываются языком, то мы можем создать значения типа listf:

# let huit_div = (/) 8 ;; val huit_div : int -> int = <fun> # let gl = Fun (succ, (Fun (huit_div, Val 4))) ;; val gl : int listf = Fun (<fun>, Fun (<fun>, Val 4))

функция сопоставляющая подобные значения:

# let rec compute = function Val v -> v | Fun(f, x) -> f (compute x) ;; val compute : 'a listf -> 'a = <fun> # compute gl;; - : int = 3

1.2.9  Пример: реализация деревьев

Деревья — одна из часто встречающихся структур в программировании. Рекурсивные типы позволяют с легкостью определять подобные вещи. В этой части мы приведем два примера с такими структурами.

Бинарные деревья

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

# type 'a arbre_bin = Empty | Node of 'a arbre_bin * 'a * 'a arbre_bin ;;

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

Извлечем значения узлов в виде сортированного списка при помощи инфиксного просмотра следующей функцией:

# let rec list_of_arbre = function Empty -> [] | Node(fg, r, fd) -> (list_of_arbre fg) @ (r :: (list_of_arbre fd)) ;; val list_of_arbre : 'a arbre_bin -> 'a list = <fun>

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

# let rec ajout x = function Empty -> Node(Empty, x, Empty) | Node(fg, r, fd) -> if x < r then Node(ajout x fg, r, fd) else Node(fg, r, ajout x fd) ;; val ajout : 'a -> 'a arbre_bin -> 'a arbre_bin = <fun>

Функция трансформирующая список в бинарное дерево может быть получена использованием функции ajout

# let rec arbre_of_list = function [] -> Empty | t::q -> ajout t (arbre_of_list q) ;; val arbre_of_list : 'a list -> 'a arbre_bin = <fun>

Тогда функция сортировки это всего–навсего композиция функций arbre_of_list и list_of_arbre.

# let tri x = list_of_arbre (arbre_of_list x) ;; val tri : 'a list -> 'a list = <fun> # tri [5; 8; 2; 7; 1; 0; 3; 6; 9; 4] ;; - : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]

3

Общие планарные деревья

Воспользуемся функцией определенной в модуле List (см. 7 стр. ??):

Общее планарное дерево — это дерево в котором нет ограничения на число дочерних узлов: каждому узлу ассоциируем список дочерних узлов и длина этого списка не ограничена.

# type 'a arbre = Empty | Node of 'a * 'a arbre list ;;

Пустое дерево представлено значением Empty, лист — это узел без дочерних узлов формы Node(х,[]) или Node(x, [Empty;Empty;..]). Таким образом достаточно просто написать функции, манипулирующие подобными деревьями такие как принадлежность элемента дереву или вычисление высоты дерева.

Для проверки принадлежности элемента e воспользуемся следующим алгоритмом: если дерево пустое, тогда элемент e не принадлежит этому дереву, в противном случае e принадлежит дереву только если он равен значению корня дерева или принадлежит дочерним узлам.

# let rec appartient e = function Empty -> false | Node(v, fs) -> (e=v) or (List.exists (appartient e) fs) ;; val appartient : 'a -> 'a arbre -> bool = <fun>

Для вычисления высоты дерева, напишем следующую функцию: высота пустого дерева равна 0, в противном случае его высота равна высоте самого большого под–дерева плюс 1.

# let rec hauteur = let max_list l = List.fold_left max 0 l in function Empty -> 0 | Node (_, fs) -> 1 + (max_list (List.map hauteur fs)) ;; val hauteur : 'a arbre -> int = <fun>

1.2.10  Не функциональные рекурсивные значения

Рекурсивное объявление не функциональных значений позволяет конструировать циклические структуры данных.

Следующее объявление создает циклический список одного элемента.

# let rec l = 1::l ;; val l : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; ...]

Применение рекурсивной функции к подобному списку приведет к переполнению памяти.

# size l ;; Stack overflow during evaluation (looping recursion?).

Структурное равенство таких списков может быть проверенно лишь в случае, когда физическое равенство верно.

# l=l ;; - : bool = true

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

let rec l2 = 1::l2 in l=l2 ;;

Однако, физическое равенство остается возможным.

# let rec l2 = 1::l2 in l==l2 ;; - : bool = false

Предикат == сравнивает непосредственно значение или разделения структурного объекта (одним словом равенство по адресу). Мы воспользуемся этим тестом для того чтобы при просмотре списка, не проверять уже просмотренные под-списки. Сначала, определим функцию memq которая проверяет присутствие элемента в списке при помощи физического равенства. В это же время функция mem проверяет равенство структурное. Обе функции принадлежат модулю List.

# let rec memq a l = match l with [] -> false | b::l -> (a==b) or (memq a l) ;; val memq : 'a -> 'a list -> bool = <fun>

Функция вычисляющая размер списка определена при помощи списка уже проверенных списков и останавливается при встрече списка второй раз.

# let special_size l = let rec size_aux previous l = match l with [] -> 0 | _::l1 -> if memq l previous then 0 else 1 + (size_aux (l::previous) l1) in size_aux [] l ;; val special_size : 'a list -> int = <fun> # special_size [1;2;3;4] ;; - : int = 4 # special_size l ;; - : int = 1 # let rec l1 = 1::2::l2 and l2 = 1::2::l1 in special_size l1 ;; - : int = 4

1.3  Типизация, область определения и исключения

Вычисленный тип функции принадлежит подмножеству множества в котором она определена. Если входной аргумент функции типа int, это не значит что она сможет быть выполнена для любого переданного ей целого числа. Таких случаях мы используем механизм исключений Objective CAML. Запуск исключения провоцирует остановку вычислений, которое может быть выявлено и обработано. Для этого необходимо установить обработчик исключения, до того как исключение может быть возбуждено.

1.3.1  Частичные функции и исключения

Область определения функции соответствует множеству значений которыми функция манипулирует. Существует множество математических частичных функций, таких как например, деление или натуральный логарифм. Проблемы возникают в том случае, когда функция манипулирует более сложными структурами данных. Действительно, каков будет результат функции определяющей первый элемент списка, если список пуст. Аналогично, вычисление факториала для отрицательного числа приводит к бесконечному вычислению.

Определенное число исключительных ситуаций может произойти во время выполнения программы, как например деление на ноль. Попытка деления на ноль может привести в лучшем случае к остановке программы и в худшем к неправильному (некоэрантному) состоянию машины. Безопасность языка программирования заключается в гарантии того, что такие случаи не возникнут. Исключение есть один из способов такой гарантии.

Деление 1 на 0 провоцирует запуск специального исключения:

1/0;; Uncaught exception: Division_by_zero

Сообщение Uncaught exception: Division_by_zero указывает во первых на то, что исключение Division_by_zero возбуждено и во вторых, что оно не было обработано.

Часто, тип функции не соответствует области ее определения в том случае когда сопоставление с образцом не является полным, то есть когда он не отслеживает все возможные случаи. Чтобы избежать такой ситуации, Objective CAML выводит следующее сообщение.

# let tete l = match l with t::q -> t ;; Characters 14-36: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: [] val tete : 'a list -> 'a = <fun>

Однако, если программист все таки настаивает на своей версии, то Objective CAML воспользуется механизмом исключения в случае неправильного вызова частичной функции.

# tete [] ;; Uncaught exception: Match_failure("", 14, 36)

Мы уже встречали другое исключение определенное в Objective CAML: Failure, с входным аргументом типа string. Запустить это исключение можно функцией failwith. Воспользуемся этим и дополним определение функции tete.

# let tete = function [] -> failwith "List is empty" | h::t -> h;; val tete : 'a list -> 'a = <fun> # tete [] ;; Uncaught exception: Failure("List is empty")

1.3.2  Определения исключения

В Objective CAML, тип исключений — exn. Это особенный тип — расширяемая сумма: мы можем увеличить множество значений этого типа объявляя новые конструкторы. Такая особенность позволяет программисту определять свои собственные исключения.

Синтаксис объявления следующий:

Синтаксис:

exception Nom ;;

Синтаксис:

exception Nom of t ;;

Вот несколько примеров объявления исключений.

# exception A_MOI;; exception A_MOI # A_MOI;; - : exn = A_MOI # exception Depth of int;; exception Depth of int # Depth 4;; - : exn = Depth(4)

Имена исключений являются конструкторами — это значит они должны начинаться с заглавной буквы.

# exception minuscule ;; Characters 11-20: Syntax error


Warning
Исключения мономорфны, при объявлении типа аргумента мы не можем указать параметризующий тип.

# exception Value of 'a ;; Characters 20-22: Unbound type parameter 'a

Полиморфное исключение позволило бы определение функций, возвращающих результат любого типа. Подобный пример мы рассмотрим далее на стр. ??.

1.3.3  Возбуждение исключения

raise — функциональный примитив языка Objective CAML, ее входной аргумент есть исключение, а возвращает он полиморфный тип.

# raise ;; - : exn -> 'a = <fun> # raise A_MOI;; Uncaught exception: A_MOI 1+(raise IS_Mine);; Uncaught exception: A_MOI # raise (Depth 4);; Uncaught exception: Depth(4)

В Objective CAML нет возможности написать функцию raise, она должна быть определена системой.

1.3.4  Перехват исключения

Весь интерес возбуждения исключений содержится в возможности их обработать и изменить выполнение программы в зависимости от этого исключения. В этом случае, порядок вычисления выражения имеет значение для того чтобы определить какое исключения было запущенно. Таким образом мы выходим из рамок чисто функционального программирования, потому как порядок вычисления аргументов может изменить результат. Об этом мы поговорим в следующей главе (стр. ??)

Следующая конструкция, вычисляющая значение какого–то выражения, отлавливает исключение, возбужденого во время этого вычисления.

Синтаксис:

try expr with |p_1 ->expr_1 : | pn -> expr_n

Если вычисление expr не возбудит исключения, то тип результата будет типом подсчитываемым этим выражением. Иначе, в приведенном выше сопоставлении будет искаться ответвление соответствующее этому исключению и будет возвращено значение следующего за ним выражения.

Если ни одно ответвление не соответствует исключению, то оно будет переправлено до следующего обработчика try–with установленного в программе. Таким образом сопоставление исключения всегда исчерпывающий. Подразумевается что последний фильтр |e->raise e. Если ни одного обработчика не найдено в программе, система сама займется обработкой этого исключения и остановит программу с выводом сообщения об ошибке.

Важно понимать разницу между вычислением исключения (то есть значение типа exn) и его обработкой, которое приостанавливает вычисления. Исключение, как и любой другой тип, может быть возвращено функцией.

# let rendre x = Failure x ;; val rendre : string -> exn = <fun> # rendre "test" ;; - : exn = Failure("test") # let declencher x = raise (Failure x) ;; val declencher : string -> 'a = <fun> # declencher "test" ;; Uncaught exception: Failure("test")

Как вы наверно заметили, функция declencher (запустить) не возвращает значение типа exn, тогда как rendre (вернуть) возвращает.

Вычисления с исключениями

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

# exception Found_zero ;; exception Found_zero # let rec mult_rec l = match l with [] -> 1 | 0 :: _ -> raise Found_zero | n :: x -> n * (mult_rec x) ;; val mult_rec : int list -> int = <fun> # let mult_list l = try mult_rec l with Found_zero -> 0 ;; val mult_list : int list -> int = <fun> # mult_list [1;2;3;0;5;6] ;; - : int = 0

Оставшиеся вычисления, то есть произведения элементов после встреченного нуля, не выполняются. После встречи raise, вычисление вновь продолжится с with.

1.4  Полиморфизм и значения возвращаемые функциями

Полиморфизм Objective CAML позволяет определить функции, тип возвращаемого выражения которых конкретно не определен. Например:

# let id x = x ;; val id : 'a -> 'a = <fun>

Однако, возвращаемый тип id зависит от типа аргумента. Таким образом, если мы применим функцию id к какому-нибудь аргументу, то механизм вывода (определения) типа сможет реализовать переменную типа 'a. Для каждого частного использования тип функции id может быть определен.

Иначе, использование жесткой статической типизации, которая обеспечивает правильность выполнения, не будет иметь смысла. Функция с полностью неопределенным типом как 'a->'b, разрешит какое попало преобразование типа, что привело бы к ошибке выполнения, так как физическое представление значений не одно и то же.

Очевидное противоречие

Мы можем определить функции, которые возвращают значение, тип которого не соответствует типу аргументов. Рассмотрим несколько примеров и проанализируем почему это не противоречит жесткой статической типизации.

Вот первый пример:

# let f x = [] ;; val f : 'a -> 'b list = <fun>

Эта функция конструирует полиморфные значения из любого типа.

# f () ;; - : '_a list = [] # f "n'importe quoi" ;; - : '_a list = []

Однако, полученное значение не совсем является не определенным — подразумевается список. Таким образом она не может использоваться где попало.

Вот три примера функций типа 'a -> 'b :

# let rec f1 x = f1 x ;; val f1 : 'a -> 'b = <fun> # let f2 x = failwith "n'importe quoi" ;; val f2 : 'a -> 'b = <fun> # let f3 x = List.hd [] ;; val f3 : 'a -> 'b = <fun>

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

Чтобы не было возможности определить функции типа 'a -> 'b, новые конструкторы исключений не должны иметь аргументы типы которых содержат переменную.

Если бы могли объявить полиморфное исключение Poly_exn типа 'a -> exn, то тогда следующая функция была бы возможна:

let f = function 0 -> raise (Poly_exn false) | n -> n+1 ;;

Таким образом, тип функции f это int->int и тип Poly_exn это 'a -> exn, теперь напишем следующее:

let g n = try f n with Poly_exn x -> x+1 ;;

Это правильно типизированная функция (поскольку аргумент Poly_exn может быть каким угодно) и вызов g 0 попытается сложить целое с булевым значением!

1.5  Калькулятор

Для того чтобы понять как организуются программы на Objective CAML, необходимо самим реализовать такую программу. Напишем калькулятор манипулирующий целыми числами при помощи 4 стандартных операций.

Для начала, определим тип key, для представления кнопки калькулятора. Всего таких кнопок будет 15: по одной для каждой операции, для каждой цифры и для кнопки равно.

# type key = Plus | Minus | Times | Div | Equals | Digit of int ;;

Заметим, что цифровые кнопки сгруппированы под одним конструктором Digit с аргументом целого типа. Таким образом, некоторые значения типа key не являются на самом деле кнопкой. Например, (Digit 32) есть значение типа key, но не представляет ни одну кнопку калькулятора.

Напишем функцию valid которая проверяет входной аргумент на принадлежность к типу key. Тип функции key->bool.

На первом этапе определим функцию проверяющую что число принадлежит интервалу 0-9. Объявим ее с именем is_digit.

# let is_digit = function x -> (x>=0) && (x<=9) ;; val is_digit : int -> bool = <fun>

Теперь функция valid фильтрующая свой аргумент тип которого key:

# let valid ky = match ky with Digit n -> is_digit n | _ -> true ;; val valid : key -> bool = <fun>

Первый фильтр применяется в случае если входной аргумент построен конструктором Digit, в этом случае он проверяется функцией is_digit. Второй фильтр применяется в любом другом случае. Напомним, что благодаря фильтру, тип фильтруемого значения обязательно будет key.

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

Состояние представлено следующим типом:

# type state = { lcd : int; (* last computation done *) lka : key; (* last key activated *) loa : key; (* last operator activated *) vpr : int (* value printed *) } ;;

В таблице 1.5 приведен пример состояний калькулятора.


 statekey
 (0,=,=,0)3
3/4(0,3,=,3)+
3/4(3,+,+,3)2
3/4(3,2,+,2)1
3/4(3,1,+,21)x
3/4(24,*,*,24)2
3/4(24,2,*,2)=
3/4(48,=,=,48) 
Table 1.5: Транзиции для 3+21*2=

Нам нужна функция evaluate с тремя аргументами: двумя операндами и оператором, она возвращает результат операции. Для этого функция фильтрует входной аргумент типа key:

# let evaluate x y ky = match ky with Plus -> x + y | Minus -> x - y | Times -> x * y | Div -> x / y | Equals -> y | Digit _ -> failwith "evaluate : no op";; val evaluate : int -> int -> key -> int = <fun>

Дадим определение транзиций, перечисляя все возможные случаи. Предположим, что текущее состояние есть квадри-плет (a,b,A,d):

Для того чтобы написать функцию transition, достаточно дословно перевести в Objective CAML предыдущее описание при помощи сопоставления входного аргумента. Кнопку с двумя сначениями обработаем локальной функцией digit_transition при помощи сопоставления.

# let transition st ky = let digit_transition n = function Digit _ -> { st with lka=ky; vpr=st.vpr*10+n } | _ -> { st with lka=ky; vpr=n } in match ky with Digit p -> digit_transition p st.lka | _ -> let res = evaluate st.lcd st.vpr st.loa in { lcd=res; lka=ky; loa=ky; vpr=res } ;; val transition : state -> key -> state = <fun>

Эта функция из state и key считает новое состояние state.

Протестируем теперь программу:

# let initial_state = { lcd=0; lka=Equals; loa=Equals; vpr=0 } ;; val initial_state : state = {lcd=0; lka=Equals; loa=Equals; vpr=0} # let state2 = transition initial_state (Digit 3) ;; val state2 : state = {lcd=0; lka=Digit 3; loa=Equals; vpr=3} # let state3 = transition state2 Plus ;; val state3 : state = {lcd=3; lka=Plus; loa=Plus; vpr=3} # let state4 = transition state3 (Digit 2) ;; val state4 : state = {lcd=3; lka=Digit 2; loa=Plus; vpr=2} # let state5 = transition state4 (Digit 1) ;; val state5 : state = {lcd=3; lka=Digit 1; loa=Plus; vpr=21} # let state6 = transition state5 Times ;; val state6 : state = {lcd=24; lka=Times; loa=Times; vpr=24} # let state7 = transition state6 (Digit 2) ;; val state7 : state = {lcd=24; lka=Digit 2; loa=Times; vpr=2} # let state8 = transition state7 Equals ;; val state8 : state = {lcd=48; lka=Equals; loa=Equals; vpr=48}

Мы можем сделать тоже самое, передав список транзиций.

# let transition_list st ls = List.fold_left transition st ls ;; val transition_list : state -> key list -> state = <fun> # let example = [ Digit 3; Plus; Digit 2; Digit 1; Times; Digit 2; Equals ] in transition_list initial_state example ;; - : state = {lcd=48; lka=Equals; loa=Equals; vpr=48}

1.6  Резюме

В этой главе мы изучили основы функционального программирования и параметризованный полиморфизм, что вместе являются основными характеристиками Objective CAML. Также мы описали синтаксис функциональной части ядра и типов. Это дало нам возможность написать первые программы. Так же мы подчеркнули глубокую разницу между типом функции и областью ее применения. Механизм исключений позволяет решить эту проблему, и в то же время вводит новый стиль программирования, где мы явно указываем манеру вычисления.


1
Not a Number
2
Некоторые встроенные функции не подчиняются этому правилу, особенно функция структурного равенства (=), которая является полиморфной (ее тип 'a -> 'a -> bool), но она исследует структуру своих аргументов для проверки равенства.

Previous Up Next