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

Содержимое удалено Содержимое добавлено
м Шаблон со списком лекций
Нет описания правки
Строка 1:
{{ОФП Содержание}}
 
= Лекция 5. «Служебные слова и синтаксис Haskell’а» =
 
Уже неоднократно подчёркивалось, что синтаксис языкаСинтаксис [[w:Haskell|Хаскеля]] чрезвычайноочень похож на синтаксис абстрактного функционального языка. При разработке Haskell’а создатели попытались собрать всё лучшее из имеющихся к этому времени функциональных языков, а такжеи отринуть всё самое плохоехудшее. В принципеКажется, это получилось...получилось…
 
НижеЗдесь рассматриваютсяпродолжается оставшиесярассмотрение служебныеслужебных словаслов, которыеначатое ещёв не[[Основы былифункционального рассмотреныпрограммирования/Основы вязыка Haskell|предыдущей лекции]]. Кроме того, по возможности будутБудут показаны различные нововведения в парадигму функционального программирования, которые были реализованыреализованные в Haskell’еХаскеле.
 
== Охрана и локальные переменные ==
 
Как было показано в третьей лекции, охранаОхрана и локальные переменные используются в функциональном программировании исключительнолишь для простоты записи и понимания текстов программ. HaskellХаскель не обошёл своим вниманием и этот аспект, в его синтаксисе имеются специальные средства, которые позволяютдающие организовыватьорганизовать охрану и использовать локальные переменные. Если надо определить функцию с охраной, то надо использовать символ вертикальной черты <code>|</code>.
Если возникла необходимость определить какую-либо функцию с использованием механизма охраны, то для этой цели необходимо использовать символ вертикальной черты « | » :
 
<code>sign x | x > 0 = 1
| x == 0 = 0
| x < 0 = -1</code>
 
Функция <code>sign в предыдущем примере</code> использует три охраняющие конструкции, каждая из которых отделена вертикальной чертой от предыдущего определения. В принципе, таких охраняющих конструкций может быть неограниченноесколько количествоугодно. Их разбор идёт, естественно, сверху вниз, и если существует непустое пересечение в определении охраны, то сработает та конструкция, которая находитсястоящая раньше (выше) в записи определения функции.
 
Для того, чтобы облегчить написание программ, и сделать их более читабельнымичитабельнее и простымипроще для понимания в том случае, когда в каком-либо определении функции записано большое количествомного [[w:клоз|клозов]], в Haskell’еХаскеле существуетиспользуется ключевое слово «<code>case»</code>. При помощи этого слова можно не записывать клозы определения функций так, как это принято в «чистом» функциональном программировании, а несколько сократить запись. Вот общий вид определения функций с ключевым словом «<code>case»</code>'ом:
 
<code>Function X1 X2 ... Xk = case (X1, X2, ..., Xk) of
(P11, P21, ..., Pk1)  Expression1
...
(P1n, P2n, ..., Pkn)  Expressionn</code>
 
В приведенном выше описанииЗдесь жирным шрифтом выделены служебные слова и символы языка.
 
Так функция, которая возвращаетвозвращающая список из первых <var>n</var> элементов заданного списка, может быть определена следующим образом при помощи служебного слова «<code>case»</code>:
 
<code>takeFirst n l = case (n, l) of (0, _) -> []
(_, []) -> []
(n, (x:xs)) -> (x) : (takeFirst (n – 1) xs)</code>
 
И такая запись будет полностью эквивалентна обычному определению функции:
 
<code>takeFirst 0 _ = []
takeFirst _ [] = []
takeFirst n (x:xs) = (x) : (takeFirst (n – 1) xs)</code>
 
Пришло время объяснить понятие '''маски подстановки'''. В Haskell’еХаскеле маску обозначают символом нижней черты « _ » (абсолютнотак такжеже, как ив в[[w:Пролог Prolog’е(язык программирования)|Прологе]]). Этот символ заменяет любой образец и является своего рода анонимнойбезымянной переменной. Если в выражении клоза нет необходимости использования переменной образца, то её можно заменить маской подстановки. При этом, естественно, происходят отложенные вычисления: то выражение, которое может быть подставлено вместо маски, не вычисляется.
 
ДругимДругой способомспособ использования охраняющих конструкций является-- использованиеприменение конструкции «<code>if</code>-<code>then</code>-<code>else</code>»., Вкоторая Haskell’ев реализованаХаскеле итоже эта возможностьподдерживается. Формально, этаеё конструкциялегко можетможно быть лекго трансформированапревратить в выражение с использованием служебного слова «<code>case»</code>. Можно даже считать, что выражение:
<code>if Exp1 then Exp2 else Exp3</code>
 
есть сокращение для
if Exp1 then Exp2 else Exp3
<code>case (Exp1) of (True) -> Exp2
(False) -> Exp3</code>
 
Естественно, что тип <code>Exp1</code> должен быть Booleanбулевым (<code>Bool в Haskell’е</code>), а типы выражений <code>Exp2</code> и <code>Exp3</code> -- совпадать (ведь именно значения этих выражений будет возвращено определяемой через конструкцию «<code>if</code>-<code>then</code>-<code>else» </code>-функцией).
Является сокращением выражения:
 
Для использования локальных переменных (в смысле функциональной парадигмы программирования) в Haskell’еХаскеле существуетесть два типавида записи. Первый тип полностью соответствуетподобен математической нотации, введенной в третьей лекции:
case (Exp1) of (True) -> Exp2
(False) -> Exp3
 
<code>let y = a * b
Естественно, что тип Exp1 должен быть Boolean (Bool в Haskell’е), а типы выражений Exp2 и Exp3 совпадать (ведь именно значения этих выражений будет возвращено определяемой через конструкцию «if-then-else» функцией).
 
Для использования локальных переменных (в смысле функциональной парадигмы программирования) в Haskell’е существует два типа записи. Первый тип полностью соответствует математической нотации, введенной в третьей лекции:
 
let y = a * b
f x = (x + y) / y
in f c + f d</code>
 
Другим способом определения локальной переменной является её описание после использования. В этом случае используется служебное слово «where», которое ставится в конце выражения:
 
ДругимДругой способомспособ определения-- описание локальной переменной является её описание после использования. ВПри этом случае используется служебное слово «<code>where»</code>, которое ставится в конце выражения:
<code>f x y | y > z = y – z
| y == z = 0
| y < z = z – y
where z = x * x</code>
 
Таким образом видноВидно, что Haskellязык поддерживает два способа записи определения локальных переменных: префиксный (присо помощисловом служебного слова «<code>let»</code>) и постфиксный (при помощи служебного слова «<code>where»</code>). Оба способа являютсяОни равнозначнымиравнозначны, их употребление зависит только от наклонностейпредпочтений программиста. Однако обычно постфиксный способ записи используется в выражениях, где также есть охрана, в то время кака префиксный способ записи используется-- в остальных случаях.
 
== Полиморфизм ==
 
В первой лекции уже было показано, что функциональная парадигма программирования поддерживает чистый или параметрический [[w:полиморфизм|полиморфизм]]. Однако большинство современных языков программирования не обходятся без так называемого полиморфизма ad hoc, или перегрузки. Например, перегрузка практически повсеместно используется для следующих целей:
 
*Литералы 1, 2, 3 и т.д. (т.е. цифры) используются как для записи и целых чисел, так и для записи чисел произвольной точности.
*Арифметические операции (например, сложение: — знак « <code>+ »</code>) используются для работы с данными различных типов (в том числе и с нечисловыми данными, например конкатенация для строк).
*Оператор сравнения (в Haskell’е знак двойного равенства — « <code>== »</code>) используется для сравнения данных различных типов.
 
Перегруженное поведение операций различноеразлично для каждого типа данных (зачастую такое поведение вообще может быть неопределено или определено ошибочно), в то время, как в параметрическом полиморфизме тип данных, вообще говоря, не важен. Однако вВ Haskell’еХаскеле есть механизм для использования перегрузки.
 
Рассмотреть возможность использования полиморфизма ad hoc проще всего на примере операции сравнения. СуществуетЕсть большое множествомного типов, для которых можно и целесообразно переопределить операцию сравнения, но для некоторых типов эта операция бесполезна. Например, сравнение функций бессмысленно,; функции необходимо вычислять и сравнивать результаты этих вычислений. Однако, например,Но может возникнуть необходимость сравнивать списки. Конечно, здесь речь идет о сравнении значений, а не сравнении указателей (как, например, это сделано в языке [[w:Java|Java]]).
 
Рассмотрим функцию <code>element</code>, которая возвращает значение истины вприсутствия зависимостиданного от того, присутствует заданный элементэлемента в заданном списке, или нет (в целяхдля стилякрасоты даннаяэта функция описана в инфиксной форме):
 
<code>x `element` [] = False
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> для работы с любым типом данных.
 
Классы типов являютсярешают решениемэту этой проблемыпроблему в Haskell’еХаскеле. Для того, чтобыЧтобы рассмотреть этот механизм в действии, далее определяется класс типов, содержащий оператор равенства.
 
<code>class Eq a where
(==) :: a -> a -> Bool</code>
 
В этой конструкции использованы служебные слова «<code>class»</code> и «<code>where»</code>, а также переменная типов <var>a</var>. Символ «<code>Eq»</code> является именем определяемого класса. Эта запись может быть прочитана следующим образом:как «Тип a является экземпляром класса <code>Eq,</code> если для этого типа перегружена операция сравнения « <code>== »</code> соответствующего типа». Необходимо отметить, что операцияОперация сравнения должна быть определена на паре объектов одного и того же типа.
 
Ограничение того факта, что тип a должен быть экземпляром класса <code>Eq</code> записывается как <code>(Eq a)</code>. Поэтому выражение <code>(Eq a)</code> не является описанием типа, но оно описывает ограничение, накладываемое на тип <var>a</var>, и это ограничение в Haskell’е называется '''контекстом'''. Контексты располагаются перед определением типов и отделяются от них символами « <code>=> »</code>:
<code>(==) :: (Eq a) => a -> a -> Bool</code>
 
Эта запись может быть прочитанаЭто какчитается «Для каждого типа a, который являетсяявляющегося экземпляром класса Eq, определена операция « <code>== »</code>, которая имеет тип <code>(a  a  Bool)</code>». Эта операция должна быть использована в функции <code>element</code>, поэтому ограничение распространяется и на неё. В этом случае необходимо явно указывать тип функции:
(==) :: (Eq a) => a -> a -> Bool
<code>element :: (Eq a) => a -> [a] -> Bool</code>
 
Этой записью декларируется тот необходимыйнеизбежный факт, что функция <code>element</code> определена не для всех типов данных, но только для тех, для которых определена соответствующая операция сравнения.
Эта запись может быть прочитана как «Для каждого типа a, который является экземпляром класса Eq, определена операция « == », которая имеет тип (a  a  Bool)». Эта операция должна быть использована в функции element, поэтому ограничение распространяется и на неё. В этом случае необходимо явно указывать тип функции:
 
Однако теперьТеперь возникает проблема определения того, какие типы являются экземплярами класса <code>Eq</code>. Для этого в Haskell’е есть служебное слово «<code>instance»</code>. Например, для того, чтобы предписать, что тип <code>Integer</code> является экземпляром класса <code>Eq</code>, необходимонадо написать:
element :: (Eq a) => a -> [a] -> Bool
<code>instance (Eq a) => Eq (Tree a)Integer where
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>».
Этой записью декларируется тот необходимый факт, что функция element определена не для всех типов данных, но только для тех, для которых определена соответствующая операция сравнения.
 
Таким же образом можно определить операцию сравнения и для бесконечных рекурсивных типов. Например, для типа <code>Tree</code> (см. [[Основы функционального программирования/Основы языка Haskell|лекцию 4]]) определение будет выглядеть следующим образомтак:
Однако теперь возникает проблема определения того, какие типы являются экземплярами класса Eq. Для этого в Haskell’е есть служебное слово «instance». Например, для того, чтобы предписать, что тип Integer является экземпляром класса Eq, необходимо написать:
 
<code>instance (Eq Integera) => Eq (Tree a) 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</code>
 
Естественно, что если в языке определено понятие класса, должно быть определена и концепция наследования. Хотя в Haskell’е под классом понимается нечто более абстрактное, чем в обычных объектно-ориентированных языках, в Haskell’е также есть и наследование. Но в то же время концепция наследования определяется столь же изощренно, что и понятие класса. Например, от определённого выше класса Eq можно унаследовать класс Ord, который будет представлять сравнимые типы данных. Его определение будет выглядеть следующим образом:
 
Естественно, что если в языке определено понятие класса, должно быть определена и концепция наследования. Хотя в Haskell’еХаскеле под классом понимается нечто более абстрактное, чем в обычных объектно-ориентированных языках, в Haskell’енём также есть и наследование. Но вВ то же время концепцияпонятие наследования определяется столь же изощренно, что и понятие класса. Например, от определённого выше класса <code>Eq</code> можно унаследовать класс <code>Ord</code>, который будет представлять сравнимые типы данных. Его определение будет выглядеть следующим образомтаким:
<code>class (Eq a) => Ord a where
(<), (>), (<=), (>=) :: a -> a -> Bool
min, max :: a -> a -> a</code>
 
Все экземпляры класса <code>Ord</code> должны определять кроме операций «меньше», «больше», «меньше или равно», «больше или равно», «минимум» и «максимум» ещё и операцию сравнения « <code>== »</code>, т.к.ибо её класс <code>Ord</code> наследует от класса <code>Eq</code>.
 
Самое удивительно заключается в томудивительное, что Haskellязык поддерживает множественное наследование. В случае использования нескольких базовыхродительских классов, всех их просто надо перечислить через запятую в соответствующей секции. При этом экземплярыЭкземпляры классов, унаследованных от нескольких базовых классов, должны, естественно, поддерживать и все методы базовых классов.
 
== Сравнение с другими языками ==
 
Хотя классы существуют во многих других языках программирования, понятие класса в Haskell’еХаскеле несколькодовольно отличаетсяособенно.
 
*HaskellХаскель разделяет определения классов и их методов, в то время кака такие языки, как [[w:C++|C++]] и [[w:Java|Java]] вместе определяют структуру данных и методы для её обработки.
*Определения методов в Haskell’еХаскеле соответствуют виртуальным функциям C++. Каждый конкретный экземпляр класса должен переопределять методы класса.
*Больше всего хаскельские классы в Haskell’е похожи на интерфейсы Java. Как и определение интерфейса, классы в Haskell’еХаскеле предоставляют протокол использования объекта, вместо определения самих объектов.
*Haskell не поддерживает стиль перегрузки функции, используемый в C++, когда функции с одним и тем же именем получают данные различных типов для обработки.
*Типы объектов в Haskell’еХаскеле не могут быть выведены неявно. В Haskell’е неНе существует базового класса для всех классов (как, например, TObject в Object Pascal’е[[w:Delphi|Delphi]]).
*C++ и Java добавляют в [[w:компиляция|скомпилированный]] код идентифицирующую информацию (например, таблицы размещения виртуальных функций). В Haskell’еХаскеле такого нет.; Вово время интерпретации (компиляции) вся необходимая информация выводится логически.
*Не существует понятия контроля за доступом: нет публичных и защищенных методов. Вместо этого Haskellязык предоставляет механизм модуляризации программ.
 
== Упражнения ==
 
1. нотации Haskell’а записатьЗаписать функции, работающие со списками (из упражнений [[Основы функционального программирования/Структуры данных и базисные операции|лекции 2]]). По возможности воспользоваться формализмами охраны и локальными переменными.
#*<code>getN</code> — функция вычленения <var>N</var>-ого элемента из заданного списка.
#*<code>listSumm</code> — функция сложения элементов двух списков. Возвращает список, составленный из сумм элементов списков-параметров. Учесть, что переданные списки могут быть разной длины.
#*<code>oddEven</code> — функция перестановки местами соседних чётных и нечётных элементов в заданном списке.
#*<code>reverse</code> — функция, обращающая список (первый элемент списка становится последним, второй — предпоследним, и так далее до последнего элемента).
#*<code>map</code> — функция применения другой переданной в качестве параметра функции ко всем элементам заданного списка.
2. нотации Haskell’а записатьЗаписать более сложные функции, работающие со списками (из упражнений [[Основы функционального программирования/Структуры данных и базисные операции — 2|лекции 3]]). При необходимости воспользоваться дополнительными функциями и определёнными в предыдущем упражнении. По возможности воспользоваться формализмами охраны и локальными переменными.
#*<code>reverseAll</code> — функция, получающая на вход списочную структуру и обращающая все её элементы, а также её саму.
#*<code>position</code> — функция, возвращающая номер первого вхождения заданного атома в список.
#*<code>set</code> — функция, возвращающая список, содержащий единичные вхождения атомов заданного списка.
#*<code>frequency</code> — функция, возвращающая список пар (символ, частота). Каждая пара определяет атом из заданного списка и частоту его вхождения в этот список.
#3. Описать следующие классы типов. При необходимости воспользоваться механизмом наследования классов.
#*<code>Show</code> — класс, объекты экземпляров которого могут быть выведены на экран.
#*<code>Number</code> — класс, описывающий числа различной природы.
#*<code>String</code> — класс, описывающий строки (списки символов).
#4. Определить типы-экземпляры классов, описанных в предыдущем задании. По возможности для каждого экземпляра класса определить методы, работающие с объектами этого класса.
#*<code>Integer</code> — тип целых чисел.
#*<code>Real</code> — тип действительных чисел.
#*<code>Complex</code> — тип комплексных чисел.
#*<code>WideString</code> — тип строк, в виде последовательности двухбайтовых символов в кодировке UNICODE.
 
=== Ответы ===
 
#Все нижеприведённые описания на Haskell’е являются лишь одними из большого ряда возможных:
== Ответы для самопроверки ==
 
1.
#Все нижеприведённые описания на Haskell’е являются лишь одними из большого ряда возможных:
#*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
Строка 188 ⟶ 182 :
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.
#Все нижеприведённые описания на Haskell’е являются лишь одними из большого ряда возможных:
#*reverseAll:
atom :: ListStr (a) -> Bool
atom a = True
Строка 202 ⟶ 196 :
reverseAll [] = []
reverseAll (h:t) = append (reverseAll t) (reverseAll h)
#*position:
position :: a -> [a] -> Integer
position a l = positionN a l 0
Строка 210 ⟶ 204 :
positionN a (h:t) n = positionN a t (n + 1)
positionN a [] n = 0
#*set:
set :: [a] -> [a]
set [] = []
Строка 219 ⟶ 213 :
include a (a:t) = a : t
include a (b:t) = b : (include a t)
#*frequency:
frequency :: [a] -> [(a : Integer)]
frequency l = f [] l
Строка 231 ⟶ 225 :
corrector a (a:n):t = (a : (n + 1)) : t
corrector a h:t = h : (corrector a t)
3.
#Все нижеприведённые описания на Haskell’е являются лишь одними из большого ряда возможных:
#*Show:
class Show a where
show :: a -> a
#*Number:
class Number a where
(+) :: a -> a -> a
Строка 241 ⟶ 235 :
(*) :: a -> a -> a
(/) :: a -> a -> a
#*String:
class String a where
(++) :: a -> a -> a
length :: a -> Integer
4.
#Все нижеприведённые описания на Haskell’е являются лишь одними из большого ряда возможных:
#*Integer:
instance Number Integer where
x + y = plusInteger x y
Строка 257 ⟶ 251 :
...
#*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