Главная » Haskell » Классы типов и экземпляры классов

0

Третий тип программных сущностей языка Haskell —  классы  типов.  Здесь имеется одна ловушка, в которую могут попасть те, кто применяет на практике объектно-ориентированный стиль. В языке Haskell, а также в некоторых схожих с ним функциональных языках программирования под термином «класс» понимается совсем не то, что подразумевает объектно-ориентированное программирование. Если в рамках объектно-ориентированного подхода под классом понимается тип данных, то в функциональном программировании (а вернее, в модели статической типизации, принятой в языке  Haskell) класс типов  —  это, скорее, интерфейс работы с данными.

Класс как  интерфейс

Итак классы типов в языке Haskell больше всего похожи на интерфейсы в таких языках, как Java или IDL. Действительно, классы типов описывают наборы методов (функций), которые применимы для работы с теми или иными типами данных, для которых объявлены экземпляры заданных классов. Чтобы не быть абстрактно-голословным, достаточно рассмотреть некоторые примеры использования классов типов в стандартном модуле Prelude.

Один из самых простых классов, которые  описаны в  стандартном модуле Prelude,  это класс Eq — класс типов сравнимых величин. Он определяется  следующим образом:

class Eq a where

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

x  == y  =  not  (x /=  y) x  /=  y  =  not  (x  == y)

Это — определение   класса с именем Eq, экземпляром которого  может быть некоторый тип a. Внутри этого класса определяются два метода в виде бинарных операций: (==) и (/=).  Эти операции имеют заданный тип. Более того, ниже приведены выражения этих операций друг через друга, что позволяет транслятору языка Haskell самостоятельно вычислять методы класса, которые выражены через другие методы или внешние функции, в случаях, когда разработчик программного  обеспечения явно не указал реализацию заявленных методов для некоторого типа.

Если рассматривать абстрактно, то по подобной декларации невозможно сказать, для чего предназначен этот класс Eq. Конечно, если рассматривать  семантику  его названия, а также сигнатуры  функций, то можно предположить, что все типы, которые  являются  экземплярами данного класса, являются типами сравнимых  величин, то есть таких значений, которые  можно сравнивать друг с другом при помощи операций (==) и (/=).

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

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

переменная типа, использованная в заголовке декларации после имени  класса. В рассмотренном примере класса Eq такой переменной является переменная a. Например, можно рассмотреть декларацию класса Ord из стандартного модуля Prelude:

class (Eq  a)  => Ord a  where compare :: a ->  a ->  Ordering  (<)          ::  a ->  a ->  Bool

(<=)        :: a ->  a  ->  Bool (>=)        ::  a ->  a ->  Bool (>)       :: a ->  a  ->  Bool max          ::  a ->  a ->  a

min         :: a  ->  a ->  a

compare x  y  | x  == y        = EQ

| x  <= y       = LT

| otherwise = GT

x  <= y  = compare x  y  /=  GT x  < y   =  compare x  y  == LT x  >= y  = compare x  y  /=  LT x  > y   =  compare x  y  == GT

max x  y  | x  <=  y        = y

| otherwise = x

min x  y  | x  <=  y        = x

| otherwise = y

Этот класс уже более сложный, чем класс Eq. В нём определяются 7 методов, четыре из которых являются бинарными операциями. Для каждого метода описывается сигнатура. Ниже, после декларации сигнатур методов, опять приводится выражение методов друг через друга. Эта секция необязательна, но если методы можно выразить друг через друга, хорошим тоном в определении классов является указание таких взаимосвязей. Это позволяет снять некоторую часть работы с тех программистов, кто будет реализовывать экземпляры классов.

Директива (Eq a)  => будет рассматриваться  ниже в разделе 3.2.. Здесь необходимо лишь упомянуть, что эта директива называется «контекстом».

Для чего необходим класс Ord? Транслятор языка Haskell никогда не ответит на этот вопрос. Он только лишь сможет перечислить методы, которые необходимо реализовать для некоторого типа данных, чтобы этот тип являлся экземпляром данного класса. Поэтому необходимо повториться о том, что семантика класса описывается вне программ на языке Haskell. Только программист, описавший класс Ord, скажет, что этот класс является классом типов упорядоченных величин. Впрочем, об этом можно догадаться по наименованию его методов.

Другими  словами, классы типов — это такая сущность, которая гарантирует для типов, являющихся экземплярами данного класса, наличия определённых методов, которые обрабатывают значения этих типов. Более того, для всех таких типов наименования методов будут одинаковыми — они будут иметь названия, определённые в классе. Собственно, наличие класса типов и является гарантией для функций о том, что определённые в классе методы существуют для обрабатываемых значений (в дальнейшем такие функции для классов будут называться

«прикладными функциями»).

То есть классы используются в языке Haskell для полиморфизма ad hoc — перегрузки имён функций. Действительно, достаточно определить экземплярами некоторого класса пару типов, чтобы одинаковые функции (методы класса) могли работать над значениями этих типов. Это — естественный  способ перегружать имена функций и пользоваться непараметрическим полиморфизмом.

С другой стороны, классы — это объединение типов в группы по назначению. В стандартном модуле Prelude, к примеру, описаны классы для целочисленных значений, для дробных значений и т. д.  Все эти классы используются для того, чтобы можно было использовать соответствующие операции над различными типами данных. И все такие типы данных являются в чём-то схожими друг с другом, поскольку являются экземплярами одинаковых классов.

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

class Logic  a where

neg       :: a ->  a           – Отрицание (<&&>) :: a ->  a ->  a  -Конъюнкция (<||>)  :: a ->  a ->  a -Дизъюнкция (<~|>)  :: a ->  a  ->  a -Исключающее  ИЛИ (<=>>)  ::  a ->  a ->  a -Импликация (<==>)  :: a ->  a ->  a – Эквивалентность

Класс Logic  определяет 6 методов, из которых пять являются  бинарными операциями. Кстати, для методов класса, являющихся по  смыслу операциями, можно пользоваться  определением приоритета и  ассоциативности при помощи ключевых слов infix,  infixl  и infixr  (см.  подраздел 1.1.4.). Это можно делать потому, что методы класса являются декларациями верхнего уровня, их видно извне класса — это такие же функции, как и прочие, определяемые разработчиком. Поэтому при определении методов класса также необходимо помнить, что такие методы не могут иметь одинаковые наименования с функциями, определяемыми вне классов.

Определить приоритет и ассоциативность перечисленных в классе Logic  бинарных операций можно следующим образом:

infixl 7  <&&>, <~|> infixr 6  <=>>,  <==> infixl 5  <||>

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

x  <&&>  y  = neg (neg  x  <||>  neg y) x  <||> y  = neg (neg  x  <&&>  neg y)

x  <~|> y  = (neg  x  <&&>  y) <||> (x <&&>  neg y) x  <=>> y  = neg x  <||> x  <&&>  y

x  <==> y  = (neg  x  <&&>  neg y) <||> x  <&&>  y

Таким образом, класс Logic  является классом типов, значения которых могут быть использованы в качестве логических значений. Например, таким типом является тип Bool. Другим типом может быть тип Float, который может использоваться для представления значений бесконечнозначных логик. В разделе 3.3.

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

Источник: Душкин Р. В., Справочник по языку Haskell. М.: ДМК Пресс, 2008. 544 с., ил.

По теме:

  • Комментарии