itmo_conspects

Лекция 4. Типы и классы типов

Создание новых типов

В языке Haskell можно создавать свои типы с помощью слова data. Например, с помощью data можно объявить перечисление значений:

data Color = Red | Green | Blue | White | Black

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

whatIsTheColor :: Color -> String
whatIsTheColor Red = "Color is Red"
whatIsTheColor Green = "Color is Green"
whatIsTheColor Blue = "Color is Blue"
whatIsTheColor White = "Color is White"
whatIsTheColor Black = "Color is Black"
whatIsTheColor _ = "😼" -- такое сопоставление избыточно, так как
                        -- перечислены все возможные значения Color

Если нет общего сопоставления с _ и какое-либо значение не указано, то при вызове такой функции выбросится исключение: "*** Exception: 1.hs:(4,1)-(7,39): Non-exhaustive patterns in function whatIsTheColor

Также с помощью слова data можно объявлять структуру - набор других типов, например, точку с двумя координатами:

data Point2D = CreatePoint Double Double
--   ^         ^                ^   ^
--   имя типа  имя              |   |
--             конструктора   типы полей

Здесь конструктор - это функция с сигнатурой CreatePoint :: Double -> Double -> Point2D

Далее написать функции, которые работают с ним, например, возвращение координаты x или y:

getX :: Point2D -> Double
getX (CreatePoint x _) = x

getY :: Point2D -> Double
getY (CreatePoint _ y) = y

Вместо числа с плавающей точкой Double может быть другой тип, тогда объявление будет выглядеть так:

data Point2D a = Point2D a a
-- здесь для удобства конструктор назван так же, как и тип

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

distance :: Point2D Double -> Point2D Double -> Double
distance (Point2D x1 y1) (Point2D x2 y2) =
    sqrt $ (x2 - x1) ^ 2 + (y2 - y1) ^ 2

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

data Point a = Point2D a a | Point3D a a a

Значение такого перечисления хранит в памяти значение лишь одного типа - либо Point2D, либо Point3D (аналог union в C++ или enum в Rust). Для работы с ним в функции можно воспользоваться сопоставление с образцом:

toList :: Point a -> [a]
toList (Point2D x y) = [x, y]
toList (Point3D x y z) = [x, y, z]

Для такого общего типа точки Point можно написать такую функцию расстояния:

distance :: Point Double -> Point Double -> Double
distance p1 p2 = sqrt . sum . map (^2) $ zipWith (-) (toList p2) (toList p1)

В этом примере такая функция отлично работает с Point2D и Point3D:

ghci> distance (Point2D 0 0) (Point2D 4 3) 
5.0
ghci> distance (Point3D 0 0 0) (Point3D 4 3 8)
9.433981132056603

Однако при вычислении расстояния между двухмерной и трехмерной точками получается число (потому что функция zipWith умеет работать со списками разной длины), хотя результат подобной операции в математике не определен:

ghci> distance (Point2D 0 0) (Point3D 4 3 8)  
5.0

Как правило, это говорит о плохом проектировании типов, но в этом случае можно применить сопоставление с образцом, чтобы предотвратить такое:

distance :: Point Double -> Point Double -> Double

distance p1@(Point2D _ _) p2@(Point2D _ _) =
    sqrt . sum . map (^2) $ zipWith (-) (toList p2) (toList p1)
distance p1@(Point3D _ _ _) p2@(Point3D _ _ _) =
    sqrt . sum . map (^2) $ zipWith (-) (toList p2) (toList p1)
distance _ _ = error("Результат не определен")

В Haskell конструкторы также могут иметь именованные аргументы, например:

data Student = Student
    { studentId   :: Int
    , studentName :: String
    , studentYear :: Int
    }

Далее к полям объекта такого типа можно обращаться по имени:

ghci> studentName (Student 1 "John Doe" 4)
"John Doe"

Компилятор Haskell сам генерирует функции с такими же именами, как имена полей, которые возвращают значение поля объекта

С помощью такого конструктора можно копировать объект с обновлением только одного (или нескольких) поля:

moveNextYear :: Student -> Student
moveNextYear s = s { studentYear = studentYear s + 1 }

Или осуществлять сопоставление с образцом по одному или нескольким полям:

isFirstYear :: Student -> Bool
isFirstYear Student { studentYear = 1 } = True
isFirstYear _ = False

Maybe и Either

Также Haskell имеет встроенные перечисления:

Псевдонимы и обертки

Также Haskell имеет еще два слова для работы с типами:

Классы типов

Помимо этого Haskell имеет механизм классов типов. Класс типов задает интерфейс (то есть набор сигнатур функций), чтобы затем представитель класса, конкретный тип, реализовывал этот интерфейс (наподобие трейтов в Rust)

Например, при сравнении новосозданных типов в Haskell выведется ошибка:

ghci> Red == Red
<interactive>:2:5: error: [GHC-39999]
    * No instance for `Eq Color' arising from a use of `=='

Здесь интерпретатор говорит, что при использовании (==) не был найден представитель Eq Color. С помощью :i можно спросить интерпретатор, что такое Eq:

ghci> :i Eq
type Eq :: * -> Constraint
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
  {-# MINIMAL (==) | (/=) #-}
        -- Defined in `GHC.Classes'
instance Eq Bool -- Defined in `GHC.Classes'
instance Eq Char -- Defined in `GHC.Classes'
-- ...

Здесь Eq - это класс типов, который объявляет функции (==) и (/=). Далее идут типы, которые реализуют Eq

Разберем несколько базовых классов типов:

Свой класс типов может определить с помощью слова class, например:

class HasArea a where
    area :: a -> Double

Далее можно определять реализацию этого класса у типов:

data Shape = Circle Double | Rectangle Double Double
    deriving (Eq, Show)

instance HasArea Shape where
    area (Circle r) = pi * r ^ 2
    area (Rectangle w h) = w * h

И функцию, которая требует, чтобы тип принадлежал классу:

printArea :: HasArea a => a -> String
printArea x = "Calculated area = " ++ (show $ area x)

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

Иерархия классов типов

А сами типы могут реализовывать несколько классов типов:

Принадлежность типов к классам типов