Pull to refresh

Введение в OCaml: The Basics [1]

Reading time10 min
Views27K
Original author: www.ocaml-tutorial.org
(предисловие от переводчика: сел учить окамл, обнаружил, что отсутствует перевод на русский язык руководства для начинающих. Восполняю этот пробел).

Основы


Комментарии


Комментарии в OCaml обозначаются символами (* и *), примерно так:
(* Это однострочный комментарий *)

(* Это комментарий
   на несколько
   строк.
*)

Другими словами, комментарии в OCaml очень похожи на комментарии в Си (/* ... */).

В настоящий момент нет однострочных комментариев (как #... в Перле или // ... в C99/C++/Java). Когда-то обсуждалась возможность использовать ## ..., и я весьма рекомендую окамловским товарищам в будущем добавить эту возможность (однако, хорошие редакторы открывают возможность использования однострочных комментариев даже сейчас).

Комментарии в OCaml вложенные, это позволяет очень просто комментировать куски кода с комментариями:
(* This code is broken ...

(* Primality test. *)
let is_prime n =
(* note to self: ask about this on the mailing lists *) XXX;;

*)

Вызов функций


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

В большинстве С-подобных языков вызов функции будет выглядеть так:
repeated ("hello", 3) /* this is C code */

Это означает «вызвать функцию repeated с двумя аргументами, первый аргумент — строка hello, второй аргумент — число 3».

Подобно остальным функциональным языкам программирования, в OCaml, запись вызовов функций и использование скобок существенно отличается, что приводит к множеству ошибок. Вот пример того же самого вызова, записанного на OCaml: repeated "hello" 3 (* this is OCaml code *).

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

Выражение repeated ("hello", 3) с точки зрения OCaml имеет смысл. Оно означает «вызвать функцию repeated с ОДНИМ аргументом, являющимся структурой „пара“, состоящей из двух элементов». Разумеется, это приведёт к ошибке, потому что функция repeated ожидает не один, а два аргумента, и первый аргумент должен быть строкой, а не парой. Но, не будем пока особо вдаваться в подробности о парах (кортежах). Вместо этого, просто запомните: использование скобок и запятых при передаче аргументов в функцию — ошибка.

Рассмотрим другую функцию — prompt_string, которая принимает строку с текстом приглашения и возвращает ввод пользователя. Мы хотим передать результат работы этой функции в функцию repeated. Вот версии на Си и OCaml:

/* C code: */
repeated (prompt_string ("Name please: "), 3)
(* OCaml code: *)
repeated (prompt_string "Name please: ") 3


Взгляните тщательнее на расстановку скобок и отсутствующую запятую. В версии на OCaml в скобки взят первый аргумент функции repeated, который является результатом вызова другой функции. Общее правило: «скобки вокруг вызова функции, а не вокруг аргументов функции». Вот ещё несколько примеров:

f 5 (g "hello") 3    (* у f три аргумента, у g - один *)
f (g 3 4)            (* у f один аргумент, у g - два *)

# repeated ("hello", 3);;     (* OCaml выдаст ошибку *)
This expression has type string * int but is here used with type string

Определение функций


Допустим, вы все знаете, как определять функции (или статические методы для Java) в привычных языках. Но как мы делаем это в OCaml?

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

let average a b =
  (a +. b) /. 2.0;;


Задайте это на верхнем уровне (toplevel) OCaml. (для этого, в unix, просто наберите команду ocaml). [Прим. пер. для ubuntu/debian sudo aptitude install ocaml, для suse/centos/fedora — sudo yum install ocaml]. Вы увидите:
# let average a b =
  (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>


Если вы присмотритесь к определению функции и тому, что написал OCaml, у вас возникнет несколько вопросов:
  • Что делают дополнительные точки с запятой в коде?
  • Что значит это всё float -> float -> float ?

Мы ответим на этот вопрос в следующих секциях, а пока я хотел бы определить такую же функцию на Си (на Java она бы выглядела похоже), и, надеюсь, вызвать ещё несколько вопросов. Вот версия на Си той же самой функции average:

double
average (double a, double b)
{
  return (a + b) / 2;
}


Сравните с более компактной версией на OCaml. Надеюсь, у вас возникли вопросы:
  • Почему мы не задали типы переменных a и b в версии на OCaml? Как OCaml определил типы? (и, вообще, OCaml знает типы, или он полностью динамически типизированный язык?)
  • В Си число 2 неявно приведено к типу double, может ли OCaml делать так же?
  • Как в OCaml записывается аналог оператора return?

ОК, вот некоторые ответы:
  • OCaml — язык со строгой статической типизацией (другими словами, никаких динамических приведений, подобных приведениями между int, float и string в Перле)
  • OCaml использует выведение типов для определения типов, так что вам не приходится делать это руками. Если вы вводите код на верхнем уровне OCaml, как в примере выше, то OCaml сообщает выведение типов в вашей функции
  • OCaml не осуществляет никаких неявных приведений типов. Если вы хотите число с плавающей запятой (float), то вы должны в явном виде писать 2.0, потому что 2 — это целое [прим. пер.: В английском дробная часть отделяется от целой точкой, а тип называется, дословно, «число с плавающей точкой», так что в OCaml для отделения целой и дробной частей используется точка]. OCaml не осуществляет автоматического конвертирования между типами int, float, string или любыми другими.
  • Как побочный эффект выведения типов в OCaml, функции (включая операторы) не могут быть перегружены. OCaml определяет + как операцию сложения целых чисел. Для сложения чисел с плавающей запятой используйте +. (внимание на точку после знака плюса). Аналогично, используются -., *., /. для прочих операций с плавающей запятой.
  • У OCaml нет оператора return — последнее выражение в функции используется как значение функции автоматически.

Мы обсудим подробнее всё это в последующих секциях и главах.

Основные типы


Тип Диапазон значений
int 31-битное знаковое целое на 32-битных системах и 63-битное знаковое целое на системах с 64-битным процессором
float Число с плавающей запятой двойной точности (IEEE), эквивалентно double в Си
bool Логический тип, значения true/false
char 8-битный символ
string Строка


OCaml использует один из битов типа int для хранения данных для автоматического управления памятью (сборки мусора). Вот почему размерность int 31 бит, а не 32 бита (63 бита для 64-битных систем). В обычном использовании это не проблема, за исключением нескольких специфичных случаев. Например, если вы считаете что-то в цикле, OCaml ограничивает количество итераций 1 миллиардом вместо 2. Это не проблема, потому что если вы считаете что-то близко к лимиту int в любом языке, вы должны использовать специальные модули для работы с большими числами (Nat и Big_int модули в OCaml). Однако, для обработки 32-битных значений (например, криптокод или код сетевого стека) OCaml предоставляет тип nativeint, соответствующий битности целого на платформе.

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

В OCaml тип char, использующийся для представления символов текста. К сожалению, тип char не поддерживает Unicode, ни в виде многобайтных кодировок, ни в виде UTF-8. Это серьёзный недостаток OCaml, который должен быть исправлен, а пока существуют обширные библиотеки для поддержки unicode, которые должны помочь.

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

Тип unit является некоторым подобием типа void в Си, но мы поговорим о нём позже.

Явная типизация против неявной



В Си-подобных языках целое превращается в число с плавающей запятой при некоторых обстоятельствах. Например, если вы напишите 1 + 2.5, то первый аргумент (целое) будет приведён к плавающей запятой, и результат так же будет плавающей запятой. Подобного можно добиться, записав явно ((double)1)+2.5.

OCaml никогда не делает неявного приведения типов В OCaml 1 + 2.5 — ошибка типа. Оператор сложения + требует двух целых аргументов и сообщает об ошибке, если один из аргументов является числом с плавающей запятой:

# 1 + 2.5;;
      ^^^
This expression has type float but is here used with type int


(В специфичном языке «перевод с французского» сообщение об ошибке означает «ты положил плавающую запятую тут, а я ожидал целое») [прим. пер.: OCaml разрабатывался французами и автор подшучивает над неудачным переводом сообщений об ошибках с французского на английский].

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

OCaml не приводит целые к плавающей запятой, так что вот это тоже ошибка:
# 1 +. 2.5;;
  ^
This expression has type int but is here used with type float


Теперь OCaml жалуется на первый аргумент.

А что делать, если нужно сложить целое число и число с плавающей запятой? (Пусть они сохранены в переменных i и f). В OCaml необходимо осуществить прямое приведение типов:

(float_of_int i) +. f;;


float_of_int — функция, принимающая целое и возвращающая число с плавающей запятой. Есть целая пачка таких функций, выполняющая подобные действия, называющихся примерно так: int_of_float, char_of_int, int_of_char, string_of_int. По большей части они делают то, что от них ожидают.

Так как конвертация int в float весьма частая, у float_of_int есть короткий псевдоним. Пример выше может быть записан как
float i +. f;;

(Обратите внимание, в отличие от Си, в OCaml и функция, и тип могут иметь одинаковое имя).

Что лучше — явное или неявное приведение?


Вы можете подумать, что явное приведение уродливо, что это нудное занятие. В чём-то вы правы, но есть как минимум два аргумента в пользу явного приведения. Во-первых, OCaml использует явное приведение типов для возможности выведения типов (см. ниже), а выведение типов — это настолько замечательная и экономящая время функция, что она очевидно перевешивает лишние нажатия кнопок при явной типизации. Во-вторых, если вы хоть раз занимались отладкой программ на Си, вы знаете, что (а) неявная типизация вызывает трудные для нахождения ошибки, и (б) большую часть времени вы сидите и пытаетесь понять, где сработала неявная типизация. Требование явной типизации помогает в отладке. Третье, некоторые приведения типов (особенно, целое <-> плавающая запятая) в реальности весьма дорогие операции. Вы делаете себе медвежью услугу, скрывая их в неявной типизации.

Обычные и рекурсивные функции


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

let rec range a b =
  if a > b then []
  else a :: range (a+1) b
  ;;

Обратите внимание — range вызывает саму себя.

Единственным различием между let и let rec является область видимости имени функции. Если бы функция из примера выше была бы определена с использованием просто let, то вызов функции range осуществил бы поиск существующей (ранее определённой) функции, называющейся range, а не прямо-сейчас-определяемой функции. Использование let (без rec) позволит вам переопределять значение в терминал предыдущего определения. Например:
let positive_sum a b = 
    let a = max a 0
    and b = max b 0 in
    a + b


Переопределение прячет предыдущую «привязку» a и b из определения функции. В некоторых ситуациях программисты предпочитают этот подход использованию новых переменных (let a_pos = max a 0) так как это делает старые привязки недоступными, оставляя доступными только новейшие значения a и b.

Определение функций через let rec не даёт каких-либо изменений в производительности по сравнению с let, так что если вам нравится, вы можете всегда использовать форму let rec, чтобы получить аналогичное Си-подобным языкам поведение.

Типизация значений функций


Благодаря выведению типов вам редко придётся явно указывать тип возвращаемого функцией значения. Однако, OCaml часто выводит что он думает о типе возвращаемого значения ваших функций, так что вы должны знать синтаксис для подобных записей. Для функции f, которая принимает аргументы arg1, arg2,… argn, и возвращает значение rettype компилятор выведет:
f : arg1 -> arg2 -> ... -> argn -> rettype

Синтаксис с использованием стрелки выглядит непривычно, но потом мы подойдём к так называемым производным функциям (currying), вы поймёте, почему он такой. Пока приведём несколько примеров.

Наша функция repeated принимает строку и целое, возвращает строку. Её тип описывается так:
repeated : string -> int -> string

Наша функция average, принимающая два числа с плавающей запятой и возвращающая одно число с плавающей запятой, описывается так:
average : float -> float -> float

Стандартная функция OCaml int_of_char приводит:
int_of_char : char -> int


Если функция ничего возвращает (void для Си и Java), то мы записываем, что она возвращает тип unit. Например, вот так выглядит на OCaml аналог функции fputs:
output_char : out_channel -> char -> unit

Полиморфные функции


Теперь чуть больше странного. Как насчёт функции, которая принимает что угодно в качестве аргумента? Вот пример ненормальной функции, которая берёт один аргумент, но игнорирует его и всегда возвращает число 3:
let give_me_a_three x = 3;;

Какой тип у этой функции? В OCaml используется специальный заместитель, означающий «что вашей душе угодно». Это одиночная кавычка с последующей буквой. Тип функции выше записывается как:
give_me_a_three : 'a -> int

где 'a в реальности означает «любой тип». Вы, например, можете вызывать эту функцию как give_me_a_three "foo" или give_me_a_three 2.0. Оба варианта будут одинаково правильными с точки зрения OCaml.

Пока что не очень понятно, почему полиморфные функции полезны, но на самом деле они очень полезны и очень распространены; мы обсудим их позже. (Подсказка: полифорфизм — это что-то вроде шаблонов в C++ или generic в Java).

Выведение типов


Темой этого учебника является мысль, о том, что функциональные языки содержат в себе Много Клёвых Фич и OCaml — это язык, который имеет все Клёвые Фичи собранными в одном месте, делая его очень полезным на практике для реальных программистов. Но вот что странно — большинство этих полезных возможностей не имеют никакого отношения к «функциональному программированию». На самом деле, я подошёл к первой Клёвой Фиче и я всё ещё не сказал ни слова про то, почему функциональное программирование называется «функциональным». В любом случае, вот первая Клёвая Фича: выведение типов.

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

В дополнение, OCaml сделает все проверки типов для вас, даже между несколькими файлами.

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

Вернёмся к функции average, которая была введена на верхнем уровне OCaml.
# let average a b =
  (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>


Mirabile dictu! OCaml всё сделал сам, определил, что функция принимает два float и возвращает float.

Как? Сперва видно, что a и b использовались в выражении (a + . b). Поскольку известно, что функция .+ требует двух аргументов с плавающей запятой, простой дедукцией можно вывести, что a и b должны оба иметь тип float.

Далее, функция /. возвращает значение float, и это фактически, то же самое, что возвращаемое значение функции average, так что average должна возвращать float. Эта цепочка размышлений приводит нас к следующему описанию функции:
average : float -> float -> float

Выведение типов, очевидно, просто для коротких программ, но оно так же работает даже для больших программ. Это экономит массу времени, потому что это удаляет целый класс ошибок, вызывающих сегфолты, NullPointerException и ClassCastException в других языках (или, что важно, но часто игнорируется, предупреждения времени исполнения в иных, наподобие Perl).
Tags:
Hubs:
Total votes 65: ↑59 and ↓6+53
Comments80

Articles