Краткое пособие по языку программирования OCaml
Содержание
Введение
Цель данного пособия - познакомить с основами OCaml, который относится к разряду функциональных (аппликативных) языков программирования.
У функциональных языков, ведущих свою "родословную" от Лиспа, сложная и интересная судьба. Эти языки, любимые в университетах, научных институтах и центрах, почти незнакомы широким массам разработчиков.
Не будучи промышленными языками программирования, функциональные языки, тем не менее, являются носителями множества интересных концепций и возможностей, с которыми необходимо познакомиться любому профессионалу в области ИТ. В разные периоды времени, когда традиционные императивные языки выходят из моды, заменяются на более современные, функциональные языки демонстрируют удивительную актуальность в любые времена. Можно утверждать, что они намного опередили свое время, а может быть именно их время ещё не пришло.
OCaml достаточно широко используется на Западе для обучения языкам программирования, курсы по нему можно найти во многих университетах мира. На странице https://ocaml.org/learn/teaching-ocaml.html приведен внушительный список университетских курсов, изучающих OCaml.
Приходится выразить сожаление, что OCaml в России почти неизвестен и практически нет книг и пособий, посвящённых ему, за исключением перевода документации и книг…
Особенности Ocaml
- Язык является мультипарадигмальным, то есть поддерживает несколько парадигм программирования. Среди них:
- функциональная
- императивная
- объектно-ориентированная.
- Язык обладает высокой степенью выразительности, облегчающей его освоение и использование.
- Пакет разработчика предоставляет возможность как компиляции кода программы в бай-код или в машинный код, а также работу с программой в режиме интерпретатора.
- Управление памятью в программах полностью автоматическое.
- Ocaml обеспечивает статическую и строгую типизацию, что обнаруживает ошибки ещё на этапе компиляции.
Загрузка и установка
Основной ресурс OCaml расположен по адресу https://ocaml.org, там же можно найти ссылки на установочные пакеты для большого числа операционных систем. После загрузки и развёртывания пакета разработчика (можно произвести установку на обычный флеш-накопитель и использовать как portable-приложение) пользователю доступны следующие программы:
ocaml
- интерпретатор;ocamlc
- компилятор в байт-код;ocamlopt
- компилятор в машинный (native) код;
Представляет интерес использование редактора MS Visual Studio Code, далее сокращенно vscode для разработки. Необходимо установить расширение редактора https://github.com/hackwaly/vscode-ocaml или https://github.com/reasonml-editor/vscode-reasonml для комфортной работы. В качестве интерпретатора можно использовать ocaml
или (что еще лучше)- utop
.
Менеджер пакетов OCaml называется opam
. С помощью этой программы устанавливается большинство библиотечных пакетов.
Настройка в Unix
Для Unix-систем, после установки соответствующих пакетов, основные программы доступны в виде команд. Можно запустить интерпретатор ocaml
и работать в REPL, а можно компилировать программу и запускать как обычное приложение.
Для редактирования исходных файлов чаще всего применяют редактор Emacs с программным режимом Tuareg, что обеспечивает подсветку исходного кода, передачу выражений в REPL и отладку.
Настройка в Windows
В операционных системах Windows можно работать в командном интерпретаторе (cmd) или, как и в любой системе, в текстовом редакторе Emacs. Нужно отметить, что требуется изменить значение системных переменных (Мой компьютер->Свойства системы->Дополнительно->Переменные среды
).
OCAMLLIB=C:\Ocaml\Lib (или другой путь) PATH=PATH:c:\Ocaml\Bin (или другой путь)
После запуска cmd можно запускать ocaml как команду.
Более подробная информация об установки и настройке приведена на странице https://ocaml.org/docs/install.html
Использование REPL
Для перехода в режим интерпретатора нужна запустить программу ocaml
. Появится приглашение командной строки (#) и система переходит в режим REPL.
OCaml version 4.07.0 #
Read Eval Print Loop является одним из самых популярных режимов работы с программами на OCaml. Вы просто набираете в интерпретаторе выражения, а потом видите результат их исполнения.
Рассмотрим примеры использования OCaml в качестве калькулятора:
# 3 + 5;;
Результат выполнения:
- : int = 8
OCaml является строготипизированным языком, так что появление int в ответе не должно удивлять. Тип результата выражения должен совпадать с типами операндов. Попробуйте смешать в одном выражение целочисленную и вещественную константы и вы немедленно получите сообщение об ошибке:
# 3 + 5.7;; Characters 2-5: 3+5.7;; ^^^ Error: This expression has type float but an expression was expected of type int
На первый взгляд, такое поведение интерпретатора может показаться странным, но не будем забывать, что именно с неявным преобразованием типов в программах начинают появляться трудноуловимые ошибки.
Знаки операций для целых и вещественных чисел отличаются: +
и +.
# 1.7 +. 7.1;; - : float = 8.7999999999999989
Для преобразования значений из одного типа в другой существуют специальные функции int_of_float и float_of_int:
# int_of_float 1.7 + 4;; - : int = 5 # 1.7 +. float_of_int 4;; - : float = 5.7
Для выхода из интерпретатора в ОС нужно выполнить команду exit 0;;
Встроенные типы данных
В качестве основных типов используются:
Название | Обозначение | Пример |
---|---|---|
Целый | int | 8 |
Вещественный | float | 3.1 |
Логический | bool | true |
Строковый | string | "Hello" |
Символьный | char | 'a' |
Операции над целыми значениями:
Название | Обозначение | Пример |
---|---|---|
Сложение | + | 8+6 |
Вычитание | - | 3-4 |
Умножение | * | 9*9 |
Деление | / | 5/2 |
Остаток от деления | mod | 5 mod 2 |
Операции над вещественными значениями:
Название | Обозначение | Пример |
---|---|---|
Сложение | +. | 8 +. 6 |
Вычитание | -. | 3 -. 4 |
Умножение | *. | 9 *. 9 |
Деление | /. | 5 /. 2 |
Возведение в степень | ** | 5 ** 2 |
Для работы с вещественными числами можно использовать встроенные функции:
Название | Обозначение | Пример |
---|---|---|
Ближайшее целое, превосходящее x | ceil | ceil x |
Ближайшее целое, не превосходящее x | floor | floor x |
Квадратный корень | sqrt | sqrt x |
Экспонента | exp | exp x |
Натуральный логарифм | log | log x |
Десятичный логарифм | log10 | log10 x |
Синус | sin | sin x |
Косинус | cos | cos x |
Тангенс | tan | tan x |
Арксинус | asin | asin x |
Арккосинус | acos | acos x |
Арктангенс | atan | atan x |
Дополнительно к типам (множественным) относят:
- кортежи
- списки
- массивы
Пример действий над строками (конкатенации):
# "Hello " ^ "world!";; - : string = "Hello world!"
Преобразование из строки в целое и обратно:
# string_of_int 12;; - : string = "12" # int_of_string "-56";; - : int = -56
Пример действий с символьным типом:
# int_of_char('A');; - : int = 65
Для работы с логическим типом имеются логические операции:
Название | Обозначение | Пример |
---|---|---|
Отрицание | not | not true |
Конъюнкция | && | true && false |
Дизъюнкция | ll | true ll false |
Для сравнения значений между собой используют такие операции:
Название | Обозначение |
---|---|
Структурное равенство | = |
Физическое равенство | == |
Отрицание = | <> |
Отрицание == | != |
Чем отличаются структурное и физическое равенства?
Структурное равенство позволяет сравнить значение полей структуры (при множественных типах), в то время как физическое сравнивает адреса в памяти. Следует помнить, что вещественные числа и строки относятся к структурным типам.
Кортежи
Кортеж представляет собой набор величин разных типов, например:
# (1,4.5,"OK");; - : int * float * string = (1, 4.5, "OK")
Здесь имеется три величины разных типов, которые OCaml определил как int*float*string
Для кортежей длины 2 (пар) можно использовать функции fst и snd:
# fst ("hello","world");; - : string = "hello" # snd ("hello","world");; - : string = "world"
С кортежами также может использоваться сопоставление с образцом:
# let tpl=("Вася",1,("Петя",34));; val tpl : string * int * (string * int) = ("Вася", 1, ("Петя", 34)) # let (name1,age1,(name2,age2))=tpl;; val name1 : string = "Вася" val age1 : int = 1 val name2 : string = "Петя" val age2 : int = 34
Списки
Списком называется набор данных одного типа.
# [1;2;3];; - : int list = [1; 2; 3]
Первой важной операцией для работы со списком является ::, которая позволяет добавить в список очередной элемент, или построить список из нескольких элементов:
# 0::[1;2;3;4];; - : int list = [0; 1; 2; 3; 4] # 1::2::3::4::5::[];; - : int list = [1; 2; 3; 4; 5]
Вторая операция @ объединяет несколько списков в один:
# [1;2;3]@[4;5;6];; - : int list = [1; 2; 3; 4; 5; 6]
Массивы
Массивом называется разновидность списка фиксированного размера
# [|1;2;3|];; - : int array = [|1; 2; 3|]
Для массивов определён специальный синтаксис, позволяющий обращаться по индексу к его элементам:
# [|1;2;3|].(0);; - : int = 1
Индексация, как и во многих других языках, производится, начиная с нуля.
Следует заметить, что строки в OCaml тоже представляют собой разновидность массивов (как в Си), так что допускается ображение к отдельным символам по индексам:
# "hello".[0];; - : char = 'h'
Условные конструкции
Как и во многих других языках в OCaml существует конструкция if..else для ветвления процесса вычисления
# if a=b then 1 else 2;; - : int = 1
Можно вкладывать условные конструкции друг в друга:
# if 3=3 then if 2=2 then 1 else 2 else 3;; - : int = 1 # if 3=4 then if 2=2 then 1 else 2 else 3;; - : int = 3
Конструкция let
С помощью конструкции let можно задавать имена различным экземплярам данных, а также объявлять функции.
Создадим именованную константу:
# let pi=3.14;; val pi : float = 3.14
В дальнейшем можно использовать имена для построения других выражений:
# let pi2=2. *. pi;; val pi2 : float = 6.28
Приведённые примеры использования let задают глобальные объявления, которые будут действовать на всём протяжении программы.
Одной из форм использования let является группировка глобальных объявлений:
# let a=1 and b=2 and c=3;; val a : int = 1 val b : int = 2 val c : int = 3
Локальное объявление создаёт связь между именем и значеним в определённой области кода:
# let a=2 in a*a;; - : int = 4
Аналогично можно использовать группировку при локальных обхявлениях:
# let a=2 and b=3 in a*b;; - : int = 6
Действие локального объявления заканчивается в тот момент, когда программа выходит из локальной области. Если существовало глобальное объявление с тем же именем, то оно восстанавливается.
let a = 5;; Printf.printf "%i\n" a;; let a = 7 in let b = a * a in Printf.printf "%i %i\n" a b;; Printf.printf "%i\n" a;;
5 7 49 5
Функции
Обычные функции
Функции играют исключительно важную роль в программировании на OCaml. Рассмотрим процесс создания новой функции. Определим функцию возведения входного значения в квадрат, а затем вызовем ее.
# let square x = x * x;; val square : int -> int = <fun> # square 2;; - : int = 4
В приведенном примере компилятор сам вывел тип функции, предположив тип входного параметра как int. Это позволило опустить описание типов.
В следующем примере компилятор тоже сам выводит все типы:
# let average a b = (a +. b) /. 2.0;;
Рекурсивные функции
В функциональных языках рекурсия играет исключительно важную роль и при помощи её разрабатывают циклические конструкции.
Определим рекурсивную функцию для вычисления факториала:
# let rec factor n = if n = 1 then 1 else n * factor (n - 1) ;; val factor : int -> int = <fun>
Теперь найдем факториал числа 5:
# factor 5;; - : int = 120
Рассмотрим задачу вычисления n-ого числа Фибоначчи:
let rec fib n = match n with 1 -> 1 | 2 -> 1 | _ -> fib (n-1) + fib (n-2) ;; val fib : int -> int = <fun>
Данная реализация страдает от эффекта экспоненциальной рекурсии и может сильно замедлять работу компьютера при больших значениях параметра.
Более экономное решение:
let fib n = let rec fib_iter i j k = if k=0 then i else fib_iter (i+j) i (k-1) in fib_iter 0 1 n ;;
В этой реализации мы использовали прием вложенности определения одной функции внутри другой (см. следующий раздел).
Рассмотрим в качестве примера функцию нахождения суммы элементов целочисленного списка
# let rec sumlist x = if x=[] then 0 else List.hd x + sumlist (List.tl x);; val sumlist : int list -> int = <fun> # sumlist [1;2;3;4;5];; - : int = 15
Основная идея данной реализации: выделение первого элемента списка и суммирование его с результатом нахождения суммы оставшейся части. Разбить список на две части помогают операции из стандартного модуля List: hd и tl (переводится как голова и хвост соответственно). С помощью рекурсии мы каждый раз будем обрабатывать всё меньшее и меньшее число элементов, пока список не станет пустым.
Отметим, что реализация процедуры нахождения суммы элементов списка может быть реализована по-другому (с помощью конструкции сопоставления с образцом).
Вложенные функции
Рассмотрим задачу проверки числа на простоту. Рассмотрим случай, когда решение состоит из двух функций:
let rec isprime_iter n j= if j*j>n then true else if n mod j==0 then false else isprime_iter n (j+1) ;; let isprime n = if n<=1 then false else isprime_iter n 2 ;;
Одна из функций рекурсивная, другая - нет. Проблема лишь в том, что первая функция является вспомогательной и нужна только внутри второй. Решением будет вложить описание вспомогательной функции в тело основной:
let isprime n = let rec isprime_iter n j= if j*j>n then true else if n mod j==0 then false else isprime_iter n (j+1) in if n<=1 then false else isprime_iter n 2 ;;
Безымянные функции
В функциональном программировании большую роль играют безымянные, или лямбда-функции.
# (fun x-> x+1);; - : int -> int = <fun>
Для того, чтобы выполнить что-то полезное с безымянной функцией, ее надо применить в точке определения:
# (fun x-> x+1) 5;; - : int = 6
Пример использования лямбда-функции для каждого элемента списка
# List.map (fun x -> x*2) [1;2;3];; - : int list = [2; 4; 6]
Пример определения функции для сложения двух чисел:
# fun x y -> x+y;; - : int -> int -> int = <fun>
Этот пример нуждается в пояснении. Здесь мы сталкиваемся с таким важным понятием функционального подхода, как каррирование. Функция, созданная для сложения двух чисел, принимает один параметр, затем возвращает функцию, которая берёт второй параметр и возвращает целое число. Схема вывода типов такая: int -> (int -> int). Данный механиз позволяет сводить вызовы функций с несколькими параметрами к вызову функций только с одним параметром.
В следующем примере мы можем увидеть запись, эквивалентную предыдущему примеру:
# fun x -> (fun y -> x+y);; - : int -> int -> int = <fun>
Разумеется, внутреннюю функцию (по y) нужно вызывать для определённого x.
В следующем примере мы создаём безымянную функцию и тут же её вызываем для двух аргументов (5 и 6):
# (fun x y -> x+y) 5 6;; - : int = 11
Используя let, можно связать с функцией имя:
# let summa = (fun x y -> x+y);; ... # summa 5 6;; - : int = 11
Поскольку создание именных функций является наиболее распространённой практикой, существует более простая конструкция с let (которую мы уже рассматривали выше):
# let summa x y = x + y;;
Пример с проверкой числа на простоту
Рассмотрим пример функции, которая получает целое число на вход и проверяет, является ли переданное число простым.
Первая реализация:
# let is_prime n = let n = abs n in let rec is_not_divisor d = d * d > n || (n mod d <> 0 && is_not_divisor (d+1)) in n <> 1 && is_not_divisor 2;;
В этой реализации внутри основной функции создается вспомогательная рекурсивная, которая проверяет остаток от деления проверяемого числа на заданный делитель. Рекурсия здесь нужна для организации цикла перебора делителей.
Вторая реализация:
let prime n = let rec checkZero x d = match d with | 1 -> true | _ -> (x mod d <> 0) && checkZero x (d-1) in match n with | 0 | 1 -> false | _ -> checkZero n (n-1) ;;
Во втором способе активно используются конструкции match.
Функции высших порядков
Если функция оперирует другими функциями как данными (например, принимает в качестве параметров), то её называют функцией высших порядков, или функционалом. Разработка и использование функционалов составляет важную часть программирования в функциональном стиле.
Рассмотрим пример, в котором безымянная функция применяется к каждому элементу списка:
# List.map (fun x -> x+1) [1;2;3;4;5];; - : int list = [2; 3; 4; 5; 6]
Функция map, относящаяся к стандартному модулю List принимает в качестве первого параметра функцию, а в качестве второго - список и применяет функцию к каждому элементу списка.
Конструкция соспоставления с образцом (match)
Очень важную роль в программах имеет конструкция match, сопоставляющую выражение с набором образцов:
# let test x= match x with | 1 -> "hello" | 2 -> "by" | _ -> "unknown";; val test : int -> string = <fun> # test 1;; - : string = "hello"
Пример с использованием сопоставления при работе со списками:
# let rec len x = match x with [] -> 0 | hd :: tl -> 1+ len(tl);; val len : 'a list -> int = <fun> # len [1;2;3;4];; - : int = 4
Пример, в котором реализуется процедура нахождения суммы элементов списка:
# let rec sumlist x = match x with [] -> 0 | hd::tl -> hd+sumlist tl;; val sumlist : int list -> int = <fun> # sumlist [1;2;3;4;5];; - : int = 15
Пример: сумма элементов списка
Вот еще одна конструкция с вложенной функцией:
let sumlist x = let rec sumlist' x res = match list with | [] -> res | hd::tl -> sumlist' tl (res + hd) sumlist' x 0
Внешняя функция sumlist содержит описание внутренней функции sumlist', которая принимает список и число. К этому числу прибавляется значение очередного элемента. Как только мы доходим до пустого списка, число-аккумулятор возвращается как результат работы всей процедуры sumlist.
Работа с source-файлами
Режим REPL - не единственный режим работы OCaml. Можно подготовить файл с исходным кодом и загрузить его в интерпретатор, а можно скомпилировать, причём компиляция осуществляется либо в бай-код, либо в машинный код.
Рассмотрим, как можно загрузить файл в интерпретатор и выполнить его.
(* Содержимое hello.ml *) Printf.printf "Hello, world!\n"
Далее, мы загружаем подготовленный файл из интерпретатора:
# #use "путь/hello.ml";; Hello, world! - : unit = ()
При использовании команды use нужно указать либо абсолютный либо относительный путь к файлу с исходным кодом.
Можно вызвать интерпретатор и передать ему имя файла (а при необходимости ещё и параметры):
$ ocaml hello.ml Hello, world!
Список источников
Ссылки
- Главный сайт: https://ocaml.org.
- PLEAC: http://pleac.sourceforge.net/pleac_ocaml/
- Try OCaml: http://try.ocamlpro.com/
- Real world OCaml book: http://dev.realworldocaml.org
- Awesome OCaml: https://github.com/ocaml-community/awesome-ocaml
Книги
- Hickey J. Introduction to Objective Caml, 2008.
- Harrop J. OCaml for scientists, 2005.
- Chailloux E., Manoury P., Pagano B. Developing applications with Objective Caml, O'Reilly, 2000.
- Chailloux E., Manoury P., Pagano B. Разработка программ с помощью Objective Caml, (русский перевод), 2007.
- Smith J. Practical OCaml, APress, 2006.
- Downey A., Monje N. Think OCaml, Green Tea Press, 2008.
- Whitington J. OCaml from the Very Beginning, Coherent press. Cambridge, 2013.
- Whitington J. More OCaml: Algorithms, Methods, and Diversions, Coherent press. Cambridge, 2014.
- Leroy X., Remy D. Unix system programming in OCaml, 2013.
- Minsky Y., Madhavapeddy A., Hickey J. Real world OCaml, O'Reilly, 2013.
- Didier Rémy Using, Understanding, and Unraveling OCaml, 2001.