Основы функционального программирования/Haskell/Служебные слова и синтаксис: различия между версиями

Материал из Викиучебника — открытых книг для открытого мира
Содержимое удалено Содержимое добавлено
Нет описания правки
Нет описания правки
Строка 1: Строка 1:
__TOC__
{{ОФП Содержание}}
{{ОФП Содержание}}


Синтаксис [[w:Haskell|Хаскела]] очень похож на синтаксис абстрактного функционального языка. При разработке Хаскела создатели попытались собрать всё лучшее из имеющихся к этому времени функциональных языков и отринуть всё худшее. Кажется, получилось...
= Лекция 5. Служебные слова и синтаксис Haskell’а =


Здесь продолжается рассмотрение служебных слов, начатое в '''[[Основы функционального программирования/Основы языка Haskell|предыдущей лекции]]'''. Будут показаны различные нововведения в парадигму функционального программирования, реализованные в Хаскеле.
Синтаксис [[w:Haskell|Хаскеля]] очень похож на синтаксис абстрактного функционального языка. При разработке Haskell’а создатели попытались собрать всё лучшее из имеющихся к этому времени функциональных языков и отринуть всё худшее. Кажется, получилось…

Здесь продолжается рассмотрение служебных слов, начатое в [[Основы функционального программирования/Основы языка Haskell|предыдущей лекции]]. Будут показаны различные нововведения в парадигму функционального программирования, реализованные в Хаскеле.


== Охрана и локальные переменные ==
== Охрана и локальные переменные ==


Охрана и локальные переменные используются в функциональном программировании лишь для простоты записи и понимания текстов программ. Хаскель не обошёл своим вниманием и этот аспект, в его синтаксисе имеются средства, дающие организовать охрану и использовать локальные переменные. Если надо определить функцию с охраной, то надо использовать символ вертикальной черты <code>|</code>.
Охрана и локальные переменные используются в функциональном программировании лишь для простоты записи и понимания текстов программ. Хаскел не обошёл своим вниманием и этот аспект, в его синтаксисе имеются средства, дающие организовать охрану и использовать локальные переменные. Если надо определить функцию с охраной, то надо использовать символ вертикальной черты <code>|</code>.


<code>sign x | x > 0 = 1
<code>sign x | x > 0 = 1
Строка 17: Строка 16:
Функция <code>sign</code> использует три охраняющие конструкции, каждая из которых отделена вертикальной чертой от предыдущего определения. В принципе, таких охраняющих конструкций может быть сколько угодно. Их разбор идёт, естественно, сверху вниз, и если существует непустое пересечение в определении охраны, то сработает конструкция, стоящая раньше (выше) в записи определения функции.
Функция <code>sign</code> использует три охраняющие конструкции, каждая из которых отделена вертикальной чертой от предыдущего определения. В принципе, таких охраняющих конструкций может быть сколько угодно. Их разбор идёт, естественно, сверху вниз, и если существует непустое пересечение в определении охраны, то сработает конструкция, стоящая раньше (выше) в записи определения функции.


Для того, чтобы облегчить написание программ и сделать их читабельнее и проще для понимания, когда в определении функции записано большое много [[w:клоз|клозов]], в Хаскеле используется ключевое слово <code>case</code>. При помощи этого слова можно не записывать клозы определения функций так, как это принято в «чистом» функциональном программировании, а несколько сократить запись. Вот общий вид определения функций с <code>case</code>'ом:
Для того, чтобы облегчить написание программ и сделать их читабельнее и проще для понимания, когда в определении функции записано большое много [[w:клоз|клозов]], в Хаскеле используется ключевое слово <code>case</code>. При помощи этого сло&$769;ва можно не записывать клозы определения функций так, как это принято в «чистом» функциональном программировании, а несколько сократить запись. Вот общий вид определения функций со служебным словом <code>case</code>:


<code>Function X1 X2 ... Xk = case (X1, X2, ..., Xk) of
<code>Function X1 X2 ... Xk = case (X1, X2, ..., Xk) of
Строка 24: Строка 23:
(P1n, P2n, ..., Pkn)  Expressionn</code>
(P1n, P2n, ..., Pkn)  Expressionn</code>


Здесь жирным выделены служебные слова и символы языка.
Здесь жирным выделены служебные слова&#769; и символы языка.


Так функция, возвращающая список из первых <var>n</var> элементов заданного списка, может быть определена при помощи <code>case</code>:
Так функция, возвращающая список из первых <math>n</math> элементов заданного списка, может быть определена при помощи <code>case</code>:


<code>takeFirst n l = case (n, l) of (0, _) -> []
<code>takeFirst n l = case (n, l) of (0, _) -> []
(_, []) -> []
(_, []) -> []
(n, (x:xs)) -> (x) : (takeFirst (n 1) xs)</code>
(n, (x:xs)) -> (x) : (takeFirst (n - 1) xs)</code>


И такая запись будет полностью эквивалентна обычному определению функции:
И такая запись будет полностью эквивалентна обычному определению функции:
Строка 38: Строка 37:
takeFirst n (x:xs) = (x) : (takeFirst (n – 1) xs)</code>
takeFirst n (x:xs) = (x) : (takeFirst (n – 1) xs)</code>


Пришло время объяснить понятие '''маски подстановки'''. В Хаскеле маску обозначают символом нижней черты _ (так же, как в [[w:Пролог (язык программирования)|Прологе]]). Этот символ заменяет любой образец и является своего рода безымянной переменной. Если в выражении клоза нет необходимости использования переменной образца, то её можно заменить маской подстановки. При этом, естественно, происходят отложенные вычисления: то выражение, которое может быть подставлено вместо маски, не вычисляется.
Пришло время объяснить понятие '''маски подстановки'''. В Хаскеле маску обозначают символом нижней черты <code>_</code> (так же, как в [[w:Пролог (язык программирования)|Прологе]]). Этот символ заменяет любой образец и является своего ро&#769;да безымянной переменной. Если в выражении клоза нет необходимости использования переменной образца, то её можно заменить маской подстановки. При этом, естественно, происходят отложенные вычисления: то выражение, которое может быть подставлено вместо маски, не вычисляется.

Другой способ использования охраняющих конструкций применение конструкции «<code>if-then-else</code>», которая в Хаскеле тоже поддерживается. Формально, её легко можно превратить в выражение с <code>case</code>. Можно даже считать, что выражение


Другой способ использования охраняющих конструкций -- применение конструкции «<code>if</code>-<code>then</code>-<code>else</code>», которая в Хаскеле тоже поддерживается. Формально, её легко можно превратить в выражение с <code>case</code>. Можно даже считать, что выражение
<code>if Exp1 then Exp2 else Exp3</code>
<code>if Exp1 then Exp2 else Exp3</code>


есть сокращение для
есть сокращение для

<code>case (Exp1) of (True) -> Exp2
<code>case (Exp1) of (True) -> Exp2
(False) -> Exp3</code>
(False) -> Exp3</code>


Естественно, что тип <code>Exp1</code> должен быть булевым (<code>Bool</code>), а типы выражений <code>Exp2</code> и <code>Exp3</code> -- совпадать (ведь именно значения этих выражений будет возвращено определяемой через конструкцию <code>if</code>-<code>then</code>-<code>else</code>-функцией).
Естественно, что тип <code>Exp1</code> должен быть булевым (<code>Bool</code>), а типы выражений <code>Exp2</code> и <code>Exp3</code> совпадать (ведь именно значения этих выражений будет возвращено определяемой через конструкцию <code>if-then-else</code> функцией).


Для использования локальных переменных (в смысле функциональной парадигмы программирования) в Хаскеле есть два вида записи. Первый полностью подобен математической нотации, введенной в третьей лекции:
Для использования локальных переменных (в смысле функциональной парадигмы программирования) в Хаскеле есть два вида записи. Первый полностью подобен математической нотации, введенной в '''[[Основы функционального программирования/Структуры данных и базисные операции — 2|третьей лекции]]''':


<code>let y = a * b
<code>let y = a * b
Строка 55: Строка 56:
in f c + f d</code>
in f c + f d</code>


Другой способ -- описание локальной переменной после использования. При этом используется служебное слово <code>where</code>, которое ставится в конце выражения:
Другой способ описание локальной переменной после использования. При этом используется служебное слово <code>where</code>, которое ставится в конце выражения:

<code>f x y | y > z = y – z
<code>f x y | y > z = y – z
| y == z = 0
| y == z = 0
Строка 61: Строка 63:
where z = x * x</code>
where z = x * x</code>


Видно, что язык поддерживает два способа записи определения локальных переменных: префиксный (со словом <code>let</code>) и постфиксный (<code>where</code>). Они равнозначны, их употребление зависит только от предпочтений программиста. Однако обычно постфиксный способ используется в выражениях, где также есть охрана, а префиксный -- в остальных случаях.
Видно, что язык поддерживает два способа записи определения локальных переменных: префиксный (со словом <code>let</code>) и постфиксный (<code>where</code>). Они равнозначны, их употребление зависит только от предпочтений программиста. Однако обычно постфиксный способ используется в выражениях, где также есть охрана, а префиксный в остальных случаях.


== Полиморфизм ==
== Полиморфизм ==


В первой лекции уже было показано, что функциональная парадигма программирования поддерживает чистый или параметрический [[w:полиморфизм|полиморфизм]]. Однако большинство современных языков не обходятся без так называемого полиморфизма ad hoc, или перегрузки. Например, перегрузка практически повсеместно используется для следующих целей:
В первой лекции уже&#769; было показано, что функциональная парадигма программирования поддерживает чистый или параметрический [[w:полиморфизм|полиморфизм]]. Однако большинство современных языков не обходятся без так называемого полиморфизма «ad hoc», или перегрузки. Например, перегрузка практически повсеместно используется для следующих целей:


*Литералы 1, 2, 3 и т.д. (т.е. цифры) используются для записи и целых чисел, и чисел произвольной точности.
*Литералы 1, 2, 3 и&nbsp;т.&nbsp;д. (т.&nbsp;е. цифры) используются для записи и целых чисел, и чисел произвольной точности.
*Арифметические операции (например, сложение: <code>+</code>) используются для работы с данными различных типов (в том числе нечисловыми, например конкатенация для строк).
*Арифметические операции (например, сложение: <code>+</code>) используются для работы с данными различных типов (в том числе нечисловыми, например конкатенация для строк).
*Оператор сравнения (<code>==</code>) используется для сравнения данных различных типов.
*Оператор сравнения (<code>==</code>) используется для сравнения данных различных типов.
Строка 73: Строка 75:
Перегруженное поведение операций различно для каждого типа данных (зачастую такое поведение вообще может быть неопределено или определено ошибочно), в то время как в параметрическом полиморфизме тип данных, вообще говоря, не важен. В Хаскеле есть механизм для использования перегрузки.
Перегруженное поведение операций различно для каждого типа данных (зачастую такое поведение вообще может быть неопределено или определено ошибочно), в то время как в параметрическом полиморфизме тип данных, вообще говоря, не важен. В Хаскеле есть механизм для использования перегрузки.


Рассмотреть возможность использования полиморфизма ad hoc проще всего на примере операции сравнения. Есть много типов, для которых можно и целесообразно переопределить операцию сравнения, но для некоторых типов эта операция бесполезна. Например, сравнение функций бессмысленно; функции необходимо вычислять и сравнивать результаты этих вычислений. Но может возникнуть необходимость сравнивать списки. Конечно, здесь речь идет о сравнении значений, а не сравнении указателей (как, например, это сделано в языке [[w:Java|Java]]).
Рассмотреть возможность использования полиморфизма «ad hoc» проще всего на примере операции сравнения. Есть много типов, для которых можно и целесообразно переопределить операцию сравнения, но для некоторых типов эта операция бесполезна. Например, сравнение функций бессмысленно; функции необходимо вычислять и сравнивать результаты этих вычислений. Но может возникнуть необходимость сравнивать списки. Конечно, здесь речь идет о сравнении значений, а не сравнении указателей (как, например, это сделано в языке [[w:Java|Java]]).


Рассмотрим функцию <code>element</code>, которая возвращает значение истины присутствия данного элемента в списке (для красоты эта функция описана в инфиксной форме):
Рассмотрим функцию <code>element</code>, которая возвращает значение истины присутствия данного элемента в списке (для красоты эта функция описана в инфиксной форме):
Строка 80: Строка 82:
x `element` (y:ys) = (x == y) || (x `element` ys)</code>
x `element` (y:ys) = (x == y) || (x `element` ys)</code>


Здесь видно, что у функции <code>element</code> тип <code>(a [a] Bool)</code>, но при этом тип операции <code>==</code> должен быть <code>(a a Bool)</code>. Раз переменная типа a может обозначать любой тип (в том числе и список), целесообразно переопределить операцию <code>==</code> для работы с любым типом данных.
Здесь видно, что у функции <code>element</code> тип <code>(a -> [a] -> Bool)</code>, но при этом тип операции <code>==</code> должен быть <code>(a -> a -> Bool)</code>. Раз переменная типа <code>a</code> может обозначать любой тип (в том числе и список), целесообразно переопределить операцию <code>==</code> для работы с любым типом данных.


Классы типов решают эту проблему в Хаскеле. Чтобы рассмотреть этот механизм в действии, далее определяется класс типов, содержащий оператор равенства.
Классы типов решают эту проблему в Хаскеле. Чтобы рассмотреть этот механизм в действии, далее определяется класс типов, содержащий оператор равенства.
Строка 87: Строка 89:
(==) :: a -> a -> Bool</code>
(==) :: a -> a -> Bool</code>


В этой конструкции использованы служебные слова <code>class</code> и <code>where</code>, а также переменная типов <var>a</var>. Символ <code>Eq</code> является именем определяемого класса. Эта запись может быть прочитана как «Тип a является экземпляром класса <code>Eq</code> если для этого типа перегружена операция сравнения <code>==</code> соответствующего типа». Операция сравнения должна быть определена на паре объектов одного и того же типа.
В этой конструкции использованы служебные слова <code>class</code> и <code>where</code>, а также переменная типов <code>a</code>. Символ <code>Eq</code> является именем определяемого класса. Эта запись может быть прочитана как «Тип <code>a</code> является экземпляром класса <code>Eq</code> если для этого типа перегружена операция сравнения <code>==</code> соответствующего типа». Операция сравнения должна быть определена на паре объектов одного и того же типа.

Ограничение того факта, что тип <code>a</code> должен быть экземпляром класса <code>Eq</code> записывается как <code>(Eq a)</code>. Поэтому выражение <code>(Eq a)</code> не является описанием типа, но описывает ограничение, накладываемое на тип <code>a</code>, и это ограничение называется '''контекстом'''. Контексты располагаются перед определением типов и отделяются от них символами <code>=></code>:


Ограничение того факта, что тип a должен быть экземпляром класса <code>Eq</code> записывается как <code>(Eq a)</code>. Поэтому выражение <code>(Eq a)</code> не является описанием типа, но описывает ограничение, накладываемое на тип <var>a</var>, и это ограничение называется '''контекстом'''. Контексты располагаются перед определением типов и отделяются от них символами <code>=></code>:
<code>(==) :: (Eq a) => a -> a -> Bool</code>
<code>(==) :: (Eq a) => a -> a -> Bool</code>


Это читается «Для каждого типа a, являющегося экземпляром класса Eq, определена операция <code>==</code>, которая имеет тип <code>(a a Bool)</code>». Эта операция должна быть использована в функции <code>element</code>, поэтому ограничение распространяется и на неё. В этом случае необходимо явно указывать тип функции:
Это читается «Для каждого типа <code>a</code>, являющегося экземпляром класса <code>Eq</code>, определена операция <code>==</code>, которая имеет тип <code>(a -> a -> Bool)</code>». Эта операция должна быть использована в функции <code>element</code>, поэтому ограничение распространяется и на неё. В этом случае необходимо явно указывать тип функции:

<code>element :: (Eq a) => a -> [a] -> Bool</code>
<code>element :: (Eq a) => a -> [a] -> Bool</code>


Строка 98: Строка 102:


Теперь возникает проблема определения того, какие типы являются экземплярами класса <code>Eq</code>. Для этого есть служебное слово <code>instance</code>. Например, чтобы предписать, что тип <code>Integer</code> является экземпляром <code>Eq</code>, надо написать:
Теперь возникает проблема определения того, какие типы являются экземплярами класса <code>Eq</code>. Для этого есть служебное слово <code>instance</code>. Например, чтобы предписать, что тип <code>Integer</code> является экземпляром <code>Eq</code>, надо написать:

<code>instance Eq Integer where
<code>instance Eq Integer where
x == y = x `integerEq` y</code>
x == y = x `integerEq` y</code>


В этом выражении определение операции <code>==</code> называется определением метода, как в [[w:объектно-ориентированное программирование|объектно-ориентированной парадигме]]. Функция <code>integerEq</code> может быть любой (и не обязательно инфиксной), лишь бы у нее был тип <code>(a a Bool)</code>. В этом случае скорее всего подойдет примитивная функция сравнения двух натуральных чисел. Прочитать написанное выражение можно следующим образом: «Тип <code>Integer</code> является экземпляром класса <code>Eq</code>, и далее приводится определение метода, который производит сравнение двух объектов типа <code>Integer</code>».
В этом выражении определение операции <code>==</code> называется определением метода, как в [[w:объектно-ориентированное программирование|объектно-ориентированной парадигме]]. Функция <code>integerEq</code> может быть любой (и не обязательно инфиксной), лишь бы у неё был тип <code>(a -> a -> Bool)</code>. В этом случае скорее всего подойдет примитивная функция сравнения двух натуральных чисел. Прочитать написанное выражение можно следующим образом: «Тип <code>Integer</code> является экземпляром класса <code>Eq</code>, и далее приводится определение метода, который производит сравнение двух объектов типа <code>Integer</code>».


Таким же образом можно определить операцию сравнения и для бесконечных рекурсивных типов. Например, для типа <code>Tree</code> (см. [[Основы функционального программирования/Основы языка Haskell|лекцию 4]]) определение будет выглядеть так:
Таким же образом можно определить операцию сравнения и для бесконечных рекурсивных типов. Например, для типа <code>Tree</code> (см. '''[[Основы функционального программирования/Основы языка Haskell|лекцию 4]]''') определение будет выглядеть так:


<code>instance (Eq a) => Eq (Tree a) where
<code>instance (Eq a) => Eq (Tree a) where
Строка 110: Строка 115:
_ == _ = False</code>
_ == _ = False</code>


Естественно, что если в языке определено понятие класса, должно быть определена и концепция наследования. Хотя в Хаскеле под классом понимается нечто более абстрактное, чем в обычных объектно-ориентированных языках, в нём также есть и наследование. В то же время понятие наследования определяется столь же изощренно, что и понятие класса. Например, от определённого выше класса <code>Eq</code> можно унаследовать класс <code>Ord</code>, который будет представлять сравнимые типы данных. Его определение будет таким:
Естественно, что если в языке определено понятие класса, должно быть определена и концепция наследования. Хотя в Хаскеле под классом понимается нечто более абстрактное, чем в обычных объектно-ориентированных языках, в нём также есть и наследование. В то же время понятие наследования определяется столь же изощрённо, что и понятие класса. Например, от определённого выше класса <code>Eq</code> можно унаследовать класс <code>Ord</code>, который будет представлять сравнимые типы данных. Его определение будет таким:

<code>class (Eq a) => Ord a where
<code>class (Eq a) => Ord a where
(<), (>), (<=), (>=) :: a -> a -> Bool
(<), (>), (<=), (>=) :: a -> a -> Bool
Строка 125: Строка 131:
*Хаскель разделяет определения классов и их методов, а такие языки, как [[w:C++|C++]] и [[w:Java|Java]] вместе определяют структуру данных и методы для её обработки.
*Хаскель разделяет определения классов и их методов, а такие языки, как [[w:C++|C++]] и [[w:Java|Java]] вместе определяют структуру данных и методы для её обработки.
*Определения методов в Хаскеле соответствуют виртуальным функциям C++. Каждый конкретный экземпляр класса должен переопределять методы класса.
*Определения методов в Хаскеле соответствуют виртуальным функциям C++. Каждый конкретный экземпляр класса должен переопределять методы класса.
*Больше всего хаскельские классы похожи на интерфейсы Java. Как и определение интерфейса, классы в Хаскеле предоставляют протокол использования объекта, вместо определения самих объектов.
*Больше всего классы Хаскела похожи на интерфейсы Java. Как и определение интерфейса, классы в Хаскеле предоставляют протокол использования объекта, вместо определения самих объектов.
*Haskell не поддерживает стиль перегрузки функции, используемый в C++, когда функции с одним и тем же именем получают данные различных типов для обработки.
*Хаскел не поддерживает стиль перегрузки функции, используемый в C++, когда функции с одним и тем же именем получают данные различных типов для обработки.
*Типы объектов в Хаскеле не могут быть выведены неявно. Не существует базового класса для всех классов (как, например, TObject в [[w:Delphi|Delphi]]).
*Типы объектов в Хаскеле не могут быть выведены неявно. Не существует базового класса для всех классов (как, например, TObject в [[w:Delphi|Delphi]]).
*C++ и Java добавляют в [[w:компиляция|скомпилированный]] код идентифицирующую информацию (например, таблицы размещения виртуальных функций). В Хаскеле такого нет; во время интерпретации (компиляции) вся необходимая информация выводится логически.
*C++ и Java добавляют в [[w:компиляция|скомпилированный]] код идентифицирующую информацию (например, таблицы размещения виртуальных функций). В Хаскеле такого нет; во время интерпретации (компиляции) вся необходимая информация выводится логически.
Строка 133: Строка 139:
== Упражнения ==
== Упражнения ==


1. Записать функции, работающие со списками (из упражнений [[Основы функционального программирования/Структуры данных и базисные операции|лекции 2]]). По возможности воспользоваться формализмами охраны и локальными переменными.
#Записать функции, работающие со списками (из упражнений '''[[Основы функционального программирования/Структуры данных и базисные операции|лекции 2]]'''). По возможности воспользоваться формализмами охраны и локальными переменными.
*<code>getN</code> — функция вычленения <var>N</var>-ого элемента из заданного списка.
#*<code>getN</code> — функция вычленения <var>N</var>-ого элемента из заданного списка.
*<code>listSumm</code> — функция сложения элементов двух списков. Возвращает список, составленный из сумм элементов списков-параметров. Учесть, что переданные списки могут быть разной длины.
#*<code>listSumm</code> — функция сложения элементов двух списков. Возвращает список, составленный из сумм элементов списков-параметров. Учесть, что переданные списки могут быть разной длины.
*<code>oddEven</code> — функция перестановки местами соседних чётных и нечётных элементов в заданном списке.
#*<code>oddEven</code> — функция перестановки местами соседних чётных и нечётных элементов в заданном списке.
*<code>reverse</code> — функция, обращающая список (первый элемент списка становится последним, второй — предпоследним, и так далее до последнего элемента).
#*<code>reverse</code> — функция, обращающая список (первый элемент списка становится последним, второй — предпоследним, и так далее до последнего элемента).
*<code>map</code> — функция применения другой переданной в качестве параметра функции ко всем элементам заданного списка.
#*<code>map</code> — функция применения другой переданной в качестве параметра функции ко всем элементам заданного списка.
2. Записать более сложные функции, работающие со списками (из упражнений [[Основы функционального программирования/Структуры данных и базисные операции — 2|лекции 3]]). При необходимости воспользоваться дополнительными функциями и определёнными в предыдущем упражнении. По возможности воспользоваться формализмами охраны и локальными переменными.
#Записать более сложные функции, работающие со списками (из упражнений '''[[Основы функционального программирования/Структуры данных и базисные операции — 2|лекции 3]]'''). При необходимости воспользоваться дополнительными функциями и определёнными в предыдущем упражнении. По возможности воспользоваться формализмами охраны и локальными переменными.
*<code>reverseAll</code> — функция, получающая на вход списочную структуру и обращающая все её элементы, а также её саму.
#*<code>reverseAll</code> — функция, получающая на вход списочную структуру и обращающая все её элементы, а также её саму.
*<code>position</code> — функция, возвращающая номер первого вхождения заданного атома в список.
#*<code>position</code> — функция, возвращающая номер первого вхождения заданного атома в список.
*<code>set</code> — функция, возвращающая список, содержащий единичные вхождения атомов заданного списка.
#*<code>set</code> — функция, возвращающая список, содержащий единичные вхождения атомов заданного списка.
*<code>frequency</code> — функция, возвращающая список пар (символ, частота). Каждая пара определяет атом из заданного списка и частоту его вхождения в этот список.
#*<code>frequency</code> — функция, возвращающая список пар (символ, частота). Каждая пара определяет атом из заданного списка и частоту его вхождения в этот список.
3. Описать следующие классы типов. При необходимости воспользоваться механизмом наследования классов.
#Описать следующие классы типов. При необходимости воспользоваться механизмом наследования классов.
*<code>Show</code> — класс, объекты экземпляров которого могут быть выведены на экран.
#*<code>Show</code> — класс, объекты экземпляров которого могут быть выведены на экран.
*<code>Number</code> — класс, описывающий числа различной природы.
#*<code>Number</code> — класс, описывающий числа различной природы.
*<code>String</code> — класс, описывающий строки (списки символов).
#*<code>String</code> — класс, описывающий строки (списки символов).
4. Определить типы-экземпляры классов, описанных в предыдущем задании. По возможности для каждого экземпляра класса определить методы, работающие с объектами этого класса.
#Определить типы-экземпляры классов, описанных в предыдущем задании. По возможности для каждого экземпляра класса определить методы, работающие с объектами этого класса.
*<code>Integer</code> — тип целых чисел.
#*<code>Integer</code> — тип целых чисел.
*<code>Real</code> — тип действительных чисел.
#*<code>Real</code> — тип действительных чисел.
*<code>Complex</code> — тип комплексных чисел.
#*<code>Complex</code> — тип комплексных чисел.
*<code>WideString</code> — тип строк, в виде последовательности двухбайтовых символов в кодировке UNICODE.
#*<code>WideString</code> — тип строк, в виде последовательности двухбайтовых символов в кодировке UNICODE.


=== Ответы ===
== Ответы ==


Все нижеприведённые описания на Haskell’е являются лишь одними из большого ряда возможных:
Все нижеприведённые описания на Хаскеле являются лишь одними из большого ряда возможных:


1.
1.
*getN:
*<code>getN</code>:
getN :: [a] -> a
<code>getN :: [a] -> a
getN n [] = _
getN n [] = _
getN 1 (h:t) = h
getN 1 (h:t) = h
getN n (h:t) = getN (n – 1) t
getN n (h:t) = getN (n – 1) t</code>
*listSumm:
*<code>listSumm</code>:
listSumm :: Ord (a) => [a] -> [a]
<code>listSumm :: Ord (a) => [a] -> [a]
listSumm [] l = l
listSumm [] l = l
listSumm l [] = l
listSumm l [] = l
listSumm (h1:t1) (h2:t2) = (h1 + h2) : (listSumm t1 t2)
listSumm (h1:t1) (h2:t2) = (h1 + h2) : (listSumm t1 t2)</code>
*oddEven:
*<code>oddEven</code>:
oddEven :: [a] -> [a]
<code>oddEven :: [a] -> [a]
oddEven [] = []
oddEven [] = []
oddEven [x] = [x]
oddEven [x] = [x]
oddEven (h1:(h2:t)) = (h2:h1) : (oddEven t)
oddEven (h1:(h2:t)) = (h2:h1) : (oddEven t)</code>
*reverse:
*<code>reverse</code>:
append :: [a] -> [a] -> [a]
<code>append :: [a] -> [a] -> [a]
append [] l = l
append [] l = l
append (h:t) l2 = h : (append t l2)
append (h:t) l2 = h : (append t l2)
reverse :: [a] -> [a]
reverse :: [a] -> [a]
reverse [] = []
reverse [] = []
reverse (h:t) = append (reverse t [h])
reverse (h:t) = append (reverse t [h])</code>
*map:
*<code>map</code>:
map :: (a -> b) -> [a] -> [b]
<code>map :: (a -> b) -> [a] -> [b]
map f [] = []
map f [] = []
map f (h:t) = (f h) : (map f t)
map f (h:t) = (f h) : (map f t)</code>
2.
2.
*reverseAll:
*<code>reverseAll</code>:
atom :: ListStr (a) -> Bool
<code>atom :: ListStr (a) -> Bool
atom a = True
atom a = True
atom _ = False
atom _ = False
reverseAll :: ListStr (a) -> ListStr (a)
reverseAll :: ListStr (a) -> ListStr (a)
reverseAll l = l
reverseAll l = l
reverseAll [] = []
reverseAll [] = []
reverseAll (h:t) = append (reverseAll t) (reverseAll h)
reverseAll (h:t) = append (reverseAll t) (reverseAll h)</code>
*position:
*<code>position</code>:
position :: a -> [a] -> Integer
<code>position :: a -> [a] -> Integer
position a l = positionN a l 0
position a l = positionN a l 0
positionN :: a -> [a] -> Integer -> Integer
positionN :: a -> [a] -> Integer -> Integer
positionN a (a:t) n = (n + 1)
positionN a (a:t) n = (n + 1)
positionN a (h:t) n = positionN a t (n + 1)
positionN a (h:t) n = positionN a t (n + 1)
positionN a [] n = 0
positionN a [] n = 0</code>
*set:
*<code>set</code>:
set :: [a] -> [a]
<code>set :: [a] -> [a]
set [] = []
set [] = []
set (h:t) = include h (set t)
set (h:t) = include h (set t)
include :: a -> [a] -> [a]
include :: a -> [a] -> [a]
include a [] = [a]
include a [] = [a]
include a (a:t) = a : t
include a (a:t) = a : t
include a (b:t) = b : (include a t)
include a (b:t) = b : (include a t)</code>
*frequency:
*<code>frequency</code>:
frequency :: [a] -> [(a : Integer)]
<code>frequency :: [a] -> [(a : Integer)]
frequency l = f [] l
frequency l = f [] l
f :: [a] -> [a] -> [(a : Integer)]
f :: [a] -> [a] -> [(a : Integer)]
f l [] = l
f l [] = l
f l (h:t) = f (corrector h l) t
f l (h:t) = f (corrector h l) t
corrector :: a -> [a] -> [(a : Integer)]
corrector :: a -> [a] -> [(a : Integer)]
corrector a [] = [(a : 1)]
corrector a [] = [(a : 1)]
corrector a (a:n):t = (a : (n + 1)) : t
corrector a (a:n):t = (a : (n + 1)) : t
corrector a h:t = h : (corrector a t)
corrector a h:t = h : (corrector a t)</code>
3.
3.
*Show:
*<code>Show</code>:
class Show a where
<code>class Show a where
show :: a -> a
show :: a -> a</code>
*Number:
*<code>Number</code>:
class Number a where
<code>class Number a where
(+) :: a -> a -> a
(+) :: a -> a -> a
(-) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
(*) :: a -> a -> a
(/) :: a -> a -> a
(/) :: a -> a -> a</code>
*String:
*<code>String</code>:
class String a where
<code>class String a where
(++) :: a -> a -> a
(++) :: a -> a -> a
length :: a -> Integer
length :: a -> Integer</code>
4.
4.
*Integer:
*<code>Integer</code>:
instance Number Integer where
<code>instance Number Integer where
x + y = plusInteger x y
x + y = plusInteger x y
x – y = minusInteger x y
x – y = minusInteger x y
x * y = multInteger x y
x * y = multInteger x y
x / y = divInteger x y
x / y = divInteger x y
plusInteger :: Integer -> Integer -> Integer
plusInteger :: Integer -> Integer -> Integer
plusInteger x y = x + y
plusInteger x y = x + y
...
...</code>
*Real:
*<code>Real</code>:
instance Number Real where
<code>instance Number Real where
x + y = plusReal x y
x + y = plusReal x y
...
...</code>
*Complex:
*<code>Complex</code>:
instance Number Complex where
<code>instance Number Complex where
x + y = plusComplex x y
x + y = plusComplex x y
...
...</code>
*WideString:
*<code>WideString</code>:
instance String WideString where
<code>instance String WideString where
x ++ y = wideConcat x y
x ++ y = wideConcat x y
length x = wideLength x
length x = wideLength x
wideConcat x y = append x y
wideConcat x y = append x y
wideLength x = length x
wideLength x = length x</code>

Версия от 13:35, 23 марта 2006

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

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

Охрана и локальные переменные

Охрана и локальные переменные используются в функциональном программировании лишь для простоты записи и понимания текстов программ. Хаскел не обошёл своим вниманием и этот аспект, в его синтаксисе имеются средства, дающие организовать охрану и использовать локальные переменные. Если надо определить функцию с охраной, то надо использовать символ вертикальной черты |.

sign x | x > 0  = 1
       | x == 0 = 0
       | x < 0  = -1

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

Для того, чтобы облегчить написание программ и сделать их читабельнее и проще для понимания, когда в определении функции записано большое много клозов, в Хаскеле используется ключевое слово case. При помощи этого сло&$769;ва можно не записывать клозы определения функций так, как это принято в «чистом» функциональном программировании, а несколько сократить запись. Вот общий вид определения функций со служебным словом case:

Function X1 X2 ... Xk = case (X1, X2, ..., Xk) of
(P11, P21, ..., Pk1)  Expression1
...
(P1n, P2n, ..., Pkn)  Expressionn

Здесь жирным выделены служебные слова́ и символы языка.

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

takeFirst n l = case (n, l) of (0, _) -> []
                     (_, [])          -> []
                     (n, (x:xs))      -> (x) : (takeFirst (n - 1) xs)

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

takeFirst 0 _ = []
takeFirst _ [] = []
takeFirst n (x:xs) = (x) : (takeFirst (n – 1) xs)

Пришло время объяснить понятие маски подстановки. В Хаскеле маску обозначают символом нижней черты _ (так же, как в Прологе). Этот символ заменяет любой образец и является своего ро́да безымянной переменной. Если в выражении клоза нет необходимости использования переменной образца, то её можно заменить маской подстановки. При этом, естественно, происходят отложенные вычисления: то выражение, которое может быть подставлено вместо маски, не вычисляется.

Другой способ использования охраняющих конструкций — применение конструкции «if-then-else», которая в Хаскеле тоже поддерживается. Формально, её легко можно превратить в выражение с case. Можно даже считать, что выражение

if Exp1 then Exp2 else Exp3

есть сокращение для

case (Exp1) of (True)  -> Exp2
               (False) -> Exp3

Естественно, что тип Exp1 должен быть булевым (Bool), а типы выражений Exp2 и Exp3 — совпадать (ведь именно значения этих выражений будет возвращено определяемой через конструкцию if-then-else функцией).

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

let y = a * b
    f x = (x + y) / y
in f c + f d

Другой способ — описание локальной переменной после использования. При этом используется служебное слово where, которое ставится в конце выражения:

f x y | y > z  = y – z
      | y == z = 0
      | y < z  = z – y
  where z = x * x

Видно, что язык поддерживает два способа записи определения локальных переменных: префиксный (со словом let) и постфиксный (where). Они равнозначны, их употребление зависит только от предпочтений программиста. Однако обычно постфиксный способ используется в выражениях, где также есть охрана, а префиксный — в остальных случаях.

Полиморфизм

В первой лекции уже́ было показано, что функциональная парадигма программирования поддерживает чистый или параметрический полиморфизм. Однако большинство современных языков не обходятся без так называемого полиморфизма «ad hoc», или перегрузки. Например, перегрузка практически повсеместно используется для следующих целей:

  • Литералы 1, 2, 3 и т. д. (т. е. цифры) используются для записи и целых чисел, и чисел произвольной точности.
  • Арифметические операции (например, сложение: +) используются для работы с данными различных типов (в том числе нечисловыми, например конкатенация для строк).
  • Оператор сравнения (==) используется для сравнения данных различных типов.

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

Рассмотреть возможность использования полиморфизма «ad hoc» проще всего на примере операции сравнения. Есть много типов, для которых можно и целесообразно переопределить операцию сравнения, но для некоторых типов эта операция бесполезна. Например, сравнение функций бессмысленно; функции необходимо вычислять и сравнивать результаты этих вычислений. Но может возникнуть необходимость сравнивать списки. Конечно, здесь речь идет о сравнении значений, а не сравнении указателей (как, например, это сделано в языке Java).

Рассмотрим функцию element, которая возвращает значение истины присутствия данного элемента в списке (для красоты эта функция описана в инфиксной форме):

x `element` [] = False
x `element` (y:ys) = (x == y) || (x `element` ys)

Здесь видно, что у функции element тип (a -> [a] -> Bool), но при этом тип операции == должен быть (a -> a -> Bool). Раз переменная типа a может обозначать любой тип (в том числе и список), целесообразно переопределить операцию == для работы с любым типом данных.

Классы типов решают эту проблему в Хаскеле. Чтобы рассмотреть этот механизм в действии, далее определяется класс типов, содержащий оператор равенства.

class Eq a where
 (==) :: a -> a -> Bool

В этой конструкции использованы служебные слова class и where, а также переменная типов a. Символ Eq является именем определяемого класса. Эта запись может быть прочитана как «Тип a является экземпляром класса Eq если для этого типа перегружена операция сравнения == соответствующего типа». Операция сравнения должна быть определена на паре объектов одного и того же типа.

Ограничение того факта, что тип a должен быть экземпляром класса Eq записывается как (Eq a). Поэтому выражение (Eq a) не является описанием типа, но описывает ограничение, накладываемое на тип a, и это ограничение называется контекстом. Контексты располагаются перед определением типов и отделяются от них символами =>:

(==) :: (Eq a) => a -> a -> Bool

Это читается «Для каждого типа a, являющегося экземпляром класса Eq, определена операция ==, которая имеет тип (a -> a -> Bool)». Эта операция должна быть использована в функции element, поэтому ограничение распространяется и на неё. В этом случае необходимо явно указывать тип функции:

element :: (Eq a) => a -> [a] -> Bool

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

Теперь возникает проблема определения того, какие типы являются экземплярами класса Eq. Для этого есть служебное слово instance. Например, чтобы предписать, что тип Integer является экземпляром Eq, надо написать:

instance Eq Integer where
  x == y = x `integerEq` y

В этом выражении определение операции == называется определением метода, как в объектно-ориентированной парадигме. Функция integerEq может быть любой (и не обязательно инфиксной), лишь бы у неё был тип (a -> a -> Bool). В этом случае скорее всего подойдет примитивная функция сравнения двух натуральных чисел. Прочитать написанное выражение можно следующим образом: «Тип Integer является экземпляром класса Eq, и далее приводится определение метода, который производит сравнение двух объектов типа Integer».

Таким же образом можно определить операцию сравнения и для бесконечных рекурсивных типов. Например, для типа Tree (см. лекцию 4) определение будет выглядеть так:

instance (Eq a) => Eq (Tree a) where
  Leaf a == Leaf b                 = a == b
  (Branch l1 r1) == (Branch l2 r2) = (l1 == l2) && (r1 == r2)
  _ == _                           = False

Естественно, что если в языке определено понятие класса, должно быть определена и концепция наследования. Хотя в Хаскеле под классом понимается нечто более абстрактное, чем в обычных объектно-ориентированных языках, в нём также есть и наследование. В то же время понятие наследования определяется столь же изощрённо, что и понятие класса. Например, от определённого выше класса Eq можно унаследовать класс Ord, который будет представлять сравнимые типы данных. Его определение будет таким:

class (Eq a) => Ord a where
  (<), (>), (<=), (>=) :: a -> a -> Bool
  min, max             :: a -> a -> a

Все экземпляры класса Ord должны определять кроме операций «меньше», «больше», «меньше или равно», «больше или равно», «минимум» и «максимум» ещё и операцию сравнения ==, ибо её класс Ord наследует от класса Eq.

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

Сравнение с другими языками

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

  • Хаскель разделяет определения классов и их методов, а такие языки, как C++ и Java вместе определяют структуру данных и методы для её обработки.
  • Определения методов в Хаскеле соответствуют виртуальным функциям C++. Каждый конкретный экземпляр класса должен переопределять методы класса.
  • Больше всего классы Хаскела похожи на интерфейсы Java. Как и определение интерфейса, классы в Хаскеле предоставляют протокол использования объекта, вместо определения самих объектов.
  • Хаскел не поддерживает стиль перегрузки функции, используемый в C++, когда функции с одним и тем же именем получают данные различных типов для обработки.
  • Типы объектов в Хаскеле не могут быть выведены неявно. Не существует базового класса для всех классов (как, например, TObject в Delphi).
  • C++ и Java добавляют в скомпилированный код идентифицирующую информацию (например, таблицы размещения виртуальных функций). В Хаскеле такого нет; во время интерпретации (компиляции) вся необходимая информация выводится логически.
  • Не существует понятия контроля за доступом: нет публичных и защищенных методов. Вместо этого язык предоставляет механизм модуляризации программ.

Упражнения

  1. Записать функции, работающие со списками (из упражнений лекции 2). По возможности воспользоваться формализмами охраны и локальными переменными.
    • getN — функция вычленения N-ого элемента из заданного списка.
    • listSumm — функция сложения элементов двух списков. Возвращает список, составленный из сумм элементов списков-параметров. Учесть, что переданные списки могут быть разной длины.
    • oddEven — функция перестановки местами соседних чётных и нечётных элементов в заданном списке.
    • reverse — функция, обращающая список (первый элемент списка становится последним, второй — предпоследним, и так далее до последнего элемента).
    • map — функция применения другой переданной в качестве параметра функции ко всем элементам заданного списка.
  2. Записать более сложные функции, работающие со списками (из упражнений лекции 3). При необходимости воспользоваться дополнительными функциями и определёнными в предыдущем упражнении. По возможности воспользоваться формализмами охраны и локальными переменными.
    • reverseAll — функция, получающая на вход списочную структуру и обращающая все её элементы, а также её саму.
    • position — функция, возвращающая номер первого вхождения заданного атома в список.
    • set — функция, возвращающая список, содержащий единичные вхождения атомов заданного списка.
    • frequency — функция, возвращающая список пар (символ, частота). Каждая пара определяет атом из заданного списка и частоту его вхождения в этот список.
  3. Описать следующие классы типов. При необходимости воспользоваться механизмом наследования классов.
    • Show — класс, объекты экземпляров которого могут быть выведены на экран.
    • Number — класс, описывающий числа различной природы.
    • String — класс, описывающий строки (списки символов).
  4. Определить типы-экземпляры классов, описанных в предыдущем задании. По возможности для каждого экземпляра класса определить методы, работающие с объектами этого класса.
    • Integer — тип целых чисел.
    • Real — тип действительных чисел.
    • Complex — тип комплексных чисел.
    • WideString — тип строк, в виде последовательности двухбайтовых символов в кодировке UNICODE.

Ответы

Все нижеприведённые описания на Хаскеле являются лишь одними из большого ряда возможных:

1.

  • getN:
getN :: [a] -> a
getN n []    = _
getN 1 (h:t) = h
getN n (h:t) = getN (n – 1) t
  • listSumm:
listSumm :: Ord (a) => [a] -> [a]
listSumm [] l            = l
listSumm l []            = l
listSumm (h1:t1) (h2:t2) = (h1 + h2) : (listSumm t1 t2)
  • oddEven:
oddEven :: [a] -> [a]
oddEven []          = []
oddEven [x]         = [x]
oddEven (h1:(h2:t)) = (h2:h1) : (oddEven t)
  • reverse:
append :: [a] -> [a] -> [a]
append [] l     = l
append (h:t) l2 = h : (append t l2)

reverse :: [a] -> [a]
reverse []    = []
reverse (h:t) = append (reverse t [h])
  • map:
map :: (a -> b) -> [a] -> [b]
map f []    = []
map f (h:t) = (f h) : (map f t)

2.

  • reverseAll:
atom :: ListStr (a) -> Bool
atom a = True
atom _ = False

reverseAll :: ListStr (a) -> ListStr (a)
reverseAll l     = l
reverseAll []    = []
reverseAll (h:t) = append (reverseAll t) (reverseAll h)
  • position:
position :: a -> [a] -> Integer
position a l = positionN a l 0

positionN :: a -> [a] -> Integer -> Integer
positionN a (a:t) n = (n + 1)
positionN a (h:t) n = positionN a t (n + 1)
positionN a [] n    = 0
  • set:
set :: [a] -> [a]
set []    = []
set (h:t) = include h (set t)

include :: a -> [a] -> [a]
include a []    = [a]
include a (a:t) = a : t
include a (b:t) = b : (include a t)
  • frequency:
frequency :: [a] -> [(a : Integer)]
frequency l = f [] l

f :: [a] -> [a] -> [(a : Integer)]
f l []    = l
f l (h:t) = f (corrector h l) t

corrector :: a -> [a] -> [(a : Integer)]
corrector a []      = [(a : 1)]
corrector a (a:n):t = (a : (n + 1)) : t
corrector a h:t     = h : (corrector a t)

3.

  • Show:
class Show a where
  show :: a -> a
  • Number:
class Number a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  (/) :: a -> a -> a
  • String:
class String a where
  (++)   :: a -> a -> a
  length :: a -> Integer

4.

  • Integer:
instance Number Integer where
  x + y = plusInteger x y
  x – y = minusInteger x y
  x * y = multInteger x y
  x / y = divInteger x y

plusInteger :: Integer -> Integer -> Integer
plusInteger x y = x + y

...
  • Real:
instance Number Real where
  x + y = plusReal x y
  ...
  • Complex:
instance Number Complex where
  x + y = plusComplex x y
  ...
  • WideString:
instance String WideString where
  x ++ y   = wideConcat x y
  length x = wideLength x

wideConcat x y = append x y
wideLength x   = length x