(Все примеры из этой главы могут быть сохранены в файле и вычислены: просто загрузите этот файл в GHC или Hugs)

Переменные

править

Мы уже видели, как можно использовать GHCi в качестве калькулятора. Разумеется, такой подход приемлем лишь для очень коротких вычислений. Для длинных вычислений, а также для написания программ на Haskell, нам нужно отслеживать промежуточные результаты.

Промежуточные результаты хранятся в переменных. Например, рассмотрим следующее вычисление:

 ghci> 3.1416 * 5^2
 78.53999999999999

Результат — площадь круга с радиусом 5. Это очень неудобно — каждый раз печатать или запоминать последовательность  . На самом деле, вся цель програмирования заключается в том, чтобы поручить машине бездумное повторение и механическое запоминание. Именно поэтому в Haskell определена переменная под названием pi, которая содержит в себе более дюжины цифр из записи числа pi:

 ghci> pi
 3.141592653589793
 ghci> pi * 5^2
 78.53981633974483

Другими словами, как переменная pi, так и её значение 3.141592653589793 одинаково успешно могут быть использованы в вычисленииях, они взаимозаменяемы.

Исходные файлы Haskell

править

Создайте новый файл с именем Varfun.hs в вашем любимом текстовом редакторе (расширение «hs» означает «Haskell») и вставьте туда следующее определение:

 r = 5.0

Убедитесь в отсутствии пробельных символов перед r, так как Haskell чувствителен к этому.

Теперь, перейдите в каталог с сохранённым файлом, запустите GHCi и воспользуйтесь командой :load (или, для краткости можно использовать :l):

Prelude> :load Varfun.hs
Compiling Main             ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
*Main>

Загрузка исходного файла Haskell сделает доступными из командной строки GHCi все содержащиеся там определения.

Если GHCi выдаёт ошибку «Could not find module 'Varfun.hs'», возможно, вы забыли перейти в каталог с исходным файлом. Воспользуйтесь командой :cd для того, чтобы сменить текущий каталог на тот, где находится файл «Varfun.hs».


Prelude> :cd c:\myDirectory
Prelude> :load Varfun.hs
Compiling Main             ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
*Main> 

Теперь вы можете использовать в вычислениях недавно определённую переменную r.

*Main> r
5
*Main> pi * r^2
78.53981633974483

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

Давайте добавим другое определение. Измените содержимое исходного файла на следующее:

r = 5
area = pi * r ^ 2

Сохраните файл и выполните команду :reload в GHCi, чтобы загрузить туда новое содержимое файла. (Также вместо :reload, для краткости можно исполбзовать просто :r)

*Main> :reload
Compiling Main             ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
*Main>

Теперь нам доступны две переменные: r и area:

*Main> area
78.53981633974483
*Main> area / r
15.707963267948966
 

Также возможно определение переменных прямо в командной строке GHCi без привлечение исходного файла. В общех чертах в таком синтаксисе используется ключевое слово let (например: let area = pi * 5 ^2. Несмотря на то, что мы иногда будем давать такие определения в качестве примеров, очевидно, что как только задания несколько усложнятся, подобная практика станет обременительной. Именно поэтому мы заостряем ваше внимание на использовании исходных файлов с самого начала.

 

Опытным программистам: GHC может быть использован и как компилятор (то есть вы можете использовать GHC для создания самостоятельных программ, которые могут быть запущены без интерпретатора). Как это делается, мы объясним позже.

Переменные в императивных языках

править

Если вы уже знакомы с императивными языками программирования (например, C или Python), вы уже могли заметить, что в Haskell переменные весьма отличаются от того, что вам о них известно. Теперь мы объясним суть и причины этих отличий.

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

 

Переменные не изменяются

В отличие от императивных языков, переменные в Haskell не изменяются. Однажды определённые, они никогда не изменяют своих значений: они неизменяемые (англ. Immutable). Например, нижеприведённый код работать не будет:

-- этот код не работает
r = 5
r = 2

так как он определяет одну сущность дважды, что не имеет смысла. Компилятор пожалуется на «множественные объявления r» (англ. multiple declarations of r). Вероятно, вы приучили себя понимать этот код как установку значения r в 5, а затем установку этого же значения в 2, но Haskell работает не так. Однажды определённое определено навсегда. Переменные в Haskell больше похожи на аббревиатуры длинных выражений (подобно вычислениям на бумаге), чем на название участка изменяющейся компьютерной памяти.

Другой пример, который работает не так, как вы ожидаете:

r = r + 1  -- это не увеличивает r

Вместо «увеличения переменной r», это на самом деле означает рекурсивное определение r в терминах самой себя. Мы подробно разберём рекурсию позже. Пока что запомните, что этот фрагмент кода означает кое-что совершенно отличное от того, к чему вы (возможно) привыкли.

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

 y = x * 2
 x = 3
 x = 3
 y = x * 2

Мы можем записывать определения в таком порядке, в каком захотим, и нет такого понятия, как «x объявлена до y» или наоборот. (Это — вторая причина того, почему вы не можете объявить что-либо больше одного раза: иначе возникли бы неоднозначности.)

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

Функции

править

А теперь представте, что у нас не одна, а несколько окружностей с разными радиусами, и нужно вычислить их площадь. Например, чтобы рассчитать площадь круга с радиусом 3, мы объявим новые переменные r2 и area2, и запишем их в наш файл.

r = 5
area = pi*r^2
r2 = 3
area2 = pi*r2^2

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

Функции определяют для конкретного значения аргумента (или параметра) конкретное результирующее значение. Функции в Хаскеле определяются просто и подобно определению переменных. Для того, чтобы определить функцию, слева от знака «=» мы пишем имя функции, затем через пробел локальную переменную (или несколько переменных)которая и будет использована в функции, а справа от знака «=» мы пишем выражение, определяющее результирующее значение. Например, для определения площади круга с известным радиусом определим функцию area, зависящую от параметра r

area r = pi * r^2

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

*Main> area 5
78.53981633974483
*Main> area 3
28.274333882308138
*Main> area 17
907.9202768874502

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

Круглые скобки «()» используются, чтобы сгруппировать значения. Так, например

 area (5+3)

интерпретируется так: сначала сложить 5 и 3, затем вычислить функцию от получившейся суммы; в то время как

 area 5 + 3

означает: вычислить значение функции от 5, а затем, к получившемуся результату прибавить 3.

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

areaRect a b = a * b

И воспользуемся ею:

*Main> areaRect 6 10
60

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

Для задания функций также можно использовать уже заданные функции (обратите внимание на необходимость использования скобок во второй строчке, справа от знака «=»: double — функция одной переменной, поэтому отсутствие скобок выглядело бы как желание использовать double как функцию двух переменных (одна из которых — также является функцией)):

double x    = 2*x
quadruple x = double (double x)

Область видимости переменных

править

Иногда, писать одну и ту же формулу внутри одной функции не хочется, и при этом она может нигде больше не использоваться, а занимать хорошее, короткое имя не хочется, и писать длинное имя внутри функции каждый раз тоже. Для этого используются локальные переменные, которые будут доступны только внутри функции, в которой они объявлены. Для этой цели используется оператор where. Перед этим оператором и всеми локальными переменными внутри функции отбиваются четырьмя пробелами. Так компилятор отличает локальные переменные от глобальных. Например, для вычисления площади треугольника по формуле Герона:

heron a b c = sqrt (s*(s-a)*(s-b)*(s-c))
    where
    s = (a+b+c) / 2

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

heron a b c = sqrt (s*(s-a)*(s-b)*(s-c))  -- Это работать не будет.
s = (a+b+c) / 2

это не будет работать, так как переменные a, b и c доступны только в правой части функции heron (то есть они локальны) и не будут определены для переменной s.

В следующем примере рассмотрено использование нескольких функций с локальными переменными:

areaTriangleTrig  a b c = c * height / 2   -- use trigonometry
    where
    cosa   = (b^2 + c^2 - a^2) / (2*b*c)
    sina   = sqrt (1 - cosa^2)
    height = b*sina
areaTriangleHeron a b c = result           -- use Heron's formula
    where
    result = sqrt (s*(s-a)*(s-b)*(s-c))
    s      = (a+b+c)/2

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

area r = pi * r^2
r = 0

Если вы воспользуетесь этой функцией, она не будет возвращать значение «0», потому что r внутри функции area, и вне её — это две разные, независимые переменные.

Однако, стоит заметить, что если внутри функции не определена одноимённая локальная переменная, то за неё принимается глобальная. Например:

z = 4
zat a = z*a

Если набрать это в исходном файле, затем в GHCi ввести zat 2, то интерпретатор вернёт «8». Это также позволяет говорить о переменных, как о функциях от 0 переменных.