Наши партнеры








Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

Learning OCaml, for C, C++, Perl and Java programmers by Richard W.M. Jones

Глава 1.

Перевод: И.Зенков

Содержание

  1. Комментарии
  2. Вызов функции
  3. Определение функции
  4. Основные типы
  5. Неявное преобразование типа против явного
  6. Обычные и рекурсивные функции
  7. Типы функций
  8. Выбор типа

Комментарии

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

(* This is a single-line comment. *)

(* This is a
 * multi-line
 * comment.
 *)

Другими словами договоренность о комментариях, очень схожа с оригинальным C (/* ... */).

На данный момент, правда, не существует отдельной спецификации для однострочных комментариев (как # ... в Perl или // ... в C99/C++/Java). В какой-то степени, использование ## ... спорно, но я очень рекомендовал бы ребятам из OCaml реализовать эту возможность в будущем.

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, и возвращает новую строку, оригинальную с s, n раз.

В большинстве C-производных языках, вызов функции выглядит так:

repeated ("hello", 3)  /* this is C code */

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

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

repeated "hello" 3  (* this is OCaml code *)

Обратите внимание - НЕТ никаких скобок и НЕТ никаких запятых между аргументами.

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

Теперь представим, что у нас есть функция - get_string_from_user - которая ожидает строку от пользователя, и возвращает введённый результат. Предположим мы захотим передать данный результат, функции repeated. Вот как это будет выглядеть в C и OCaml:

/* C code: */
repeated (get_string_from_user ("Please type in a string."), 3)

(* OCaml code: *)
repeated (get_string_from_user "Please type in a string.") 3

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

f 5 (g "hello") 3    (* f has three arguments, g has one argument *)
f (g 3 4)            (* f has one argument, g has two arguments *)

# repeated ("hello", 3);;     (* OCaml will spot the mistake *)
This expression has type string * int but is here used with type string

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

Вы все должно быть знаете как определяются функции (или "static method" у кофеманов) в распространённых языках. Как же это делается в OCaml?

Синтаксис OCaml приятно краток. Ниже определяется функция которая берёт два числа с плавающей точкой и возвращает среднее число.

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

Наберите это в OCaml "toplevel" (в Unix, "toplevel" вызывается командой ocaml) и вы увидите следующие:

# let average a b =
  (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>

Взглянув на завершение определения функции, и возвращённый OCaml'ом результат (помечен курсивом), у вас возможно появятся следующие вопросы:

  • Что происходит с кодом по мере выполнения?
  • Что означает float -> float -> float в дополнительной строке?

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

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

Как мы видем, наше OCaml определение гораздо короче. Возможно вы спросите:

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

Хорошо, потому немного ответов.

  • OCaml это строго статически типизированный язык (другими словами ничего такого динамического с типами не происходит, в отличии например от Perl).
  • OCaml использует специальный типовой интерфейс, для составления необходимых вам типов. Если вы используете OCaml "toplevel", как в примере выше, OCaml сообщит вам [по его мнению...] соответствующий тип вашей функции.
  • OCaml не производит, неявное преобразование типа. Если вам нужно число с плавающей точкой вы пишите 2.0, поскольку 2 это целое число.
  • Поскольку OCaml не производит неявное преобразование типа, в нём различаются операторы, например "сложить два целых числа" (это +) против "сложить два числа с плавающей точкой" (это уж +. - обратите внимание на точку после +). Тоже самое относится и к другим арифметическим операторам.
  • OCaml возвращает последнее выражение функции, то есть вам не нужно писать return как например в C.

Более детально, всё рассмотрено в следующем разделе.

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

В OCaml существуют следующие основные типы:

OCaml тип

Описание

int

31-битное целое число (со знаком), приблизительно +/- 1 миллиард.

float

Число с плавающей точкой, удвоенной точности IEEE, эквивалент double в C.

bool

Булевы значения true или false.

char

8-битный символ.

string

Строка.

unit

Пишется как ().

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

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

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

Строки это не просто списки символов. Они имеют своё собственное более эффективное внутренние представление.

Тип unit того же сорта, что и void в C, но рассмотрим мы его позже.

Неявное преобразование типа против явного

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

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

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

В "переводе с Французского" языка OCaml, сообщение об ошибке можно растолковать как: "вы передали float туда где ожидается int".

Примечание переводчика.

Разумеется сообщения OCaml написаны на английском. Просто под французским, автор подразумевает "хороший английский". Вот такая вот, путаница.

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

И в данном случае OCaml не переводит автоматически int во float, потому это тоже будет ошибка:

# 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 и возвращающая float. Существует целая "система" подобных функций, например int_of_float, char_of_int, int_of_char, string_of_int и так далее. Делают все они то, что вы от них ожидаете.

Какое преобразование типов лучше?

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

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

В отличии от C-исходных языков, в 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 и let rec, более того, если вам понравится, можете использовать let rec постоянно, таким образом получив схожую семантику с C.

Типы функций

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

f : arg1 -> arg2 -> ... -> argn -> rettype

Сейчас стрелочки кажутся несколько странными, но когда мы дойдём до так называемых "currying", вы увидите почему был выбран такой синтаксис. Но для начала я просто покажу вам несколько примеров.

Функция repeated берёт целое число и строку, а возвращает результат типа string:

repeated : string -> int -> string

Функция average берёт два числа с плавающей точкой и возвращает результат такого же типа:

average : float -> float -> float

Стандартная преобразующая функция int_of_char:

int_of_char : char -> int

Если функция ничего не возвращает (void для C и Java программистов), в таком случае пишется, что она возвращает тип unit. Вот, для примера, OCaml эквивалент fputc:

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 выражение.

Нет необходимости объяснять почему используют полиморфные функции, но то, что их используют и то, что они очень важны это факт, потому позже мы вернёмся к этой теме. Маленькая подсказка: полиморфизм похож на "templates" в C++ или "generics" в Java 1.5.

Выбор типа

Суть данного руководства в том, что функциональные языки обладают Действительно Замечательными Особенностями и, что OCaml объединил все эти Действительно Замечательные Особенности в одном, став очень практичным языком для использования настоящими программистами. Но странная вещь, что большенство всех этих замечательные особенности не имеет никакого отношения к функциональному программированию, вообще. Фактически только сейчас, я расскажу о первой Действительно Замечательной Особенности, умолчав почему функциональное программирование называется "функциональным". Так или иначе, первая Действительно Замечательная Особенность: выбор типа.

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

Кроме того OCaml продолжает проверять все ваши типы, сравнивая (даже параллельно между различными файлами).

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

Давайте теперь вернёмся к функции average которая была нами набрана в OCaml "toplevel".

# let average a b =
  (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>

Свершилось чудо! OCaml сам во всём разобрался, поняв что функция берёт два аргумента типа float и возвращает float.

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

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

average : float -> float -> float

Выбор типа для такой маленькой программы разумеется очень прост, но он работает и в больших проектах, помогая сократить время и избежать таких ошибок как NullPointerException и ClassCastException, характерные в других языках (или важные, но часто-игнорируемые предупреждения во время выполнения, как в Perl).