Читаем Учебник по Haskell полностью

Глава 16

Категориальные типы

В этой главе мы узнаем как в теории категорий определяются типы. В теории категорий типы определяют-

ся как начальные и конечные объекты в специальных категориях, которые называются алгебрами функторов.

Для понимания этой главы хорошо освежить в памяти главу о структурной рекурсии, там где мы говорили

о свёртках и развёртках.

16.1 Программирование в стиле оригами

Оригами – состоит из двух слов “свёртка” и “бумага”. При программировании в стиле оригами все функ-

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

единственный способ определения рекурсии. Этот стиль очень хорошо подходит для ленивых языков про-

граммирования, поскольку в связке:

fold f . unfold g

функции свёртки и развёртки работают синхронно. Функция развёртки не производит новых элементов

до тех пор пока они не понадобятся во внешней функции свёртки.

Помните в одной из глав мы говорили о том, что рекурсивные функции можно определять через функцию

fix.

Например так выглядит рекурсивная функция сложения всех чисел от одного до n:

sumInt :: Int -> Int

sumInt 0 = 0

sumInt n = n + sumInt (n-1)

Эту функцию мы можем переписать с помощью функции fix. При вычислении fix f будет составлено

значение

f (f (f (f ... )))

Теперь перепишем функцию sumInt через fix:

sumInt = fix $ \f n ->

case n of

0

-> 0

n

-> n + f (n - 1)

Смотрите лямбда функция в аргументе fix принимает функцию и число, а возвращает число. Тип этой

функции (Int -> Int) -> (Int -> Int). После применения функции fix мы как раз и получим функцию

типа Int -> Int. В лямбда функции рекурсивный вызов был заменён на вызов функции-параметра f.

Оказывается, что этот приём может быть применён и для рекурсивных типов данных. Мы можем создать

обобщённый тип, который обозначает рекурсивный тип:

newtype Fix f = Fix { unFix :: f (Fix f) }

В этой записи мы получаем уравнение неподвижной точки Fix f = f (Fix f), где f это некоторый тип

с параметром. Определим тип целых чисел:

240 | Глава 16: Категориальные типы

data N a = Zero | Succ a

type Nat = Fix N

Теперь создадим несколько конструкторов:

zero :: Nat

zero = Fix Zero

succ :: Nat -> Nat

succ = Fix . Succ

Сохраним эти определения в модуле Fix. hs и посмотрим в интерпретаторе на значения и их типы, ghc не

сможет вывести экземпляр Show для типа Fix, потому что он зависит от типа с параметром, а не от конкретно-

го типа. Для решения этой проблемы нам придётся определить экземпляры вручную и подключить несколько

расширений языка. Помните в главе о ленивых вычислениях мы подключали расширение BangPatterns? Нам

понадобятся:

{-# Language FlexibleContexts, UndecidableInstances #-}

Теперь определим экземпляры для Show и Eq:

instance Show (f (Fix f)) => Show (Fix f) where

show x = ”(” ++ show (unFix x) ++ ”)”

instance Eq (f (Fix f)) => Eq (Fix f) where

a == b = unFix a == unFix b

Определим списки-оригами:

data L a b = Nil | Cons a b

deriving (Show)

type List a = Fix (L a)

nil :: List a

nil = Fix Nil

infixr 5 ‘cons‘

cons :: a -> List a -> List a

cons a = Fix . Cons a

В типе L мы заменили рекурсивный тип на параметр. Затем в записи List a = Fix (L a) мы произ-

водим замыкание по параметру. Мы бесконечно вкладываем тип L a во второй параметр. Так получается

рекурсивный тип для списков. Составим какой-нибудь список:

*Fix> :r

[1 of 1] Compiling Fix

( Fix. hs, interpreted )

Ok, modules loaded: Fix.

*Fix> 1 ‘cons‘ 2 ‘cons‘ 3 ‘cons‘ nil

(Cons 1 (Cons 2 (Cons 3 (Nil))))

Спрашивается, зачем нам это нужно? Зачем нам записывать рекурсивные типы через тип Fix? Оказыва-

ется при такой записи мы можем построить универсальные функции fold и unfold, они будут работать для

любого рекурсивного типа.

Помните как мы составляли функции свёртки? Мы строили воображаемый класс, в котором сворачивае-

мый тип заменялся на параметр. Например для списка мы строили свёртку так:

class [a] b where

(:) :: a -> b -> b

[]

:: b

После этого мы легко получали тип для функции свёртки:

foldr :: (a -> b -> b) -> b -> ([a] -> b)

Программирование в стиле оригами | 241

Она принимает методы воображаемого класса, в котором тип записан с параметром, а возвращает функ-

цию из рекурсивного типа в тип параметра.

Сейчас мы выполняем эту процедуру замены рекурсивного типа на параметр в обратном порядке. Сначала

мы строим типы с параметром, а затем получаем из них рекурсивные типы с помощью конструкции Fix.

Теперь методы класса с параметром это наши конструкторы исходных классов, а рекурсивный тип записан

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

fold :: (f b -> b) -> (Fix f -> b)

Смотрите функция свёртки по-прежнему принимает методы воображаемого класса с параметром, но те-

Перейти на страницу:

Похожие книги

C++: базовый курс
C++: базовый курс

В этой книге описаны все основные средства языка С++ - от элементарных понятий до супервозможностей. После рассмотрения основ программирования на C++ (переменных, операторов, инструкций управления, функций, классов и объектов) читатель освоит такие более сложные средства языка, как механизм обработки исключительных ситуаций (исключений), шаблоны, пространства имен, динамическая идентификация типов, стандартная библиотека шаблонов (STL), а также познакомится с расширенным набором ключевых слов, используемым в .NET-программировании. Автор справочника - общепризнанный авторитет в области программирования на языках C и C++, Java и C# - включил в текст своей книги и советы программистам, которые позволят повысить эффективность их работы. Книга рассчитана на широкий круг читателей, желающих изучить язык программирования С++.

Герберт Шилдт

Программирование, программы, базы данных