Функторы
Добро пожаловать в нашу серию статей. Монады одна из тех идей, которая кажется причиной множества страхов и мучений среди множества людей пробоющих Haskell. Цель этой серии показать, что это не страшная и не сложная идея, и может быть легко разобрана делая определенные шаги.
Простой пример.
Есть простой пример с которого мы начнем наш путь. Этот код превращает входную строку типа John Doe 24
в кортеж. Мы хотим учитывать все входные варианты, поэтому результатом будет Maybe
.
tupleFromInputString :: String -> Maybe (String, String, Int)
tupleFromInputString input = if length stringComponents /= 3
then Nothing
else Just (stringComponents !! 0, stringComponents !! 1, age)
where
stringComponents = words input
age = (read (stringComponents !! 2) :: Int)
Эта простая функция принимает строку и преобразует её в параметры для имени, фамилии и возраста. Предположим у нас есть другая часть программы использующая тип данных для отображение человека вместо кортежа. Мы захотим написать функцию преобразователь между этими двумя видами. Мы так же хотим учитывать ситуацию невозможности этого преобразования. Поэтому есть другая функция, которая обработает этот случай.
data Person = Person {
firstName :: String,
lastName :: String,
age :: Int
}
personFromTuple :: (String, String, Int) -> Person
personFromTuple (fName, lName, age) = Person fName lName age
convertTuple :: Maybe (String, String, Int) -> Maybe Person
convertTuple Nothing = Nothing
convertTuple (Just t) = Just (personFromTuple t)
Изменение формата
Но, представьте, наша оригинальная программа меняется в части чтения всего списка имен:
listFromInputString :: String -> [(String, String, Int)]
listFromInputString contents = mapMaybe tupleFromInputString (lines contents)
tupleFromInputString :: String -> Maybe (String, String, Int)
...
Теперь если мы передаем результат коду используя Person
мы должны изменить тип функции convertTuple
. Она будет иметь паралельную структуру. Maybe
и List
оба действуют как хранитель других значений. Иногда, нас не заботит во что обернуты значения. Нам просто хочется преобразовать что-то лежащее под существующим значением. и затем запустим новое значение в той же обертке.
Введение в функторы
С этой идеи мы можем начать разбирать функторы. Первое и главное: Функтор это класс типа в Haskell. Для типов которые являются экземплярами функторных классов типа, они должны реализовывать простую функцию: fmap
.
fmap :: (a -> b) -> f a -> f b
Функция fmap
принимает два ввода. Первый - требует функцию для вдух типов данных. Второй параметр - хранилище первоо типа. Вывод - хранилище второго типа. Теперь взглянем на несколько различных экземпляров функторов для знакомых типов. Для списков, fmap
просто определяется как базовая функция map
:
instance Functor [] where
fmap = map
На самом деле, fmap
это обобщение соответствия. Например, тип данных Map
так же функтор. Он использует свою собственную функцию map
для fmap
. Функторы просто берут эту идею преобразования всех ниже лежащих значений и применяют их к другим типам. С этим, давайте взглянем на Maybe
как на функтор:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
Выглядит довольно похоже на нашу функцию convertTuple
. Если у нас нет значения на первом месте, тогда результат Nothing
. Если имеется значение, тогда просто применяется функция к значение и превращает её в Just
. Тип данных Either
может быть типом Maybe
с дополнительной информацией по какой причине. Он имеет схожее поведение:
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
Отметим, что параметр первого типа этого объекта исправлен. Только второй параметр значения Either
изменен с помощью fmap
. Основываясь на этих примерах, мы можем увидеть как переписать convertTuple
обощеннее:
convertTupleFunctor :: Functor f => f (String, String, Int) -> f Person
convertTupleFunctor = fmap personFromTuple
MAKINGДелаем OURсвой OWN FUNCTORSфунктор
WeМы canтак alsoже takeможем ourвзять ownсвой dataсобственный typeтип andи defineопределить anэкмзепляр instanceФунктора. ofПредположим Functor.у Supposeнас weесть haveследующий theтип followingданных, dataотражаюищй typeпапку representingдолжностных aлиц directoryправительства ofна localместах. governmentЗададим officials.его It is parameterized by the typeтипом a. ThisЭто meansзначит, weчто allowмы differentпозволяем directoriesразличным usingпапкам differentиспользовать representationsразличные ofпредставления aдолжностных person:лиц.
data GovDirectory a = GovDirectory {
mayor :: a,
interimMayor :: Maybe a,
cabinet :: Map String a,
councilMembers :: [a]
}
OneОдна partчасть ofнашего ourприложения applicationможет mightотражать representлюдей peopleс withпомощь tuples.кортежей. ItsЭто typeбудет wouldтип be GovDirectory (GovDirectory(String, String, Int)
. However,В anotherто partвремя, couldкак useдругая theчасть typeможет использовать тип GovDirectory
. Person.PersonWeМы canможем defineопределить theследующий followingэкземпляр Functorфунктора instanceдля forGovDirectory
GovDirectoryопределив byfmap
. definingТак fmap.как Sinceнаш ourтип underlyingлежащий typesвнутри areв mostlyвцелом functorsявляется themselves,функтором, thisэто justпозволяет involvesпросто callingвызывать fmap
onдля the fields!полей.
instance Functor GovDirectory where
fmap f oldDirectory = GovDirectory {
mayor = f (mayor oldDirectory),
interimMayor = fmap f (interimMayor oldDirectory),
cabinet = fmap f (cabinet oldDirectory),
councilMembers = fmap f (councilMembers oldDirectory)
}
WeТак canже alsoможно useиспользовать theинфиксный infixоператор operator <$>
asв aкачестве synonymсинонима forfmap
. fmap.Чтобы Soописать youвсё canгораздо write this more cleanly as:проще:
instance Functor GovDirectory where
fmap f oldDirectory = GovDirectory {
mayor = f (mayor oldDirectory),
interimMayor = f <$> interimMayor oldDirectory,
cabinet = f <$> cabinet oldDirectory,
councilMembers = f <$> councilMembers oldDirectory
}
NowТеперь weу haveнас ourесть ownсвой functorфунктор, instance,преобразование soтипов transformingданных theвнутри underlyingнашей dataпапки typeтеперь ofпроще. ourМы directoryможем classпросто isиспользовать easy!fmap
Weобъединив canс justнашей useфункцией fmapпреобразования, in conjunction with our transformation function, personFromTuple:personFromTuple
:
oldDirectory :: GovDirectory (String, String, Int)
oldDirectory = GovDirectory
("John", "Doe", 46)
Nothing
(M.fromList
[ ("Treasurer", ("Timothy", "Houston", 51))
, ("Historian", ("Bill", "Jefferson", 42))
, ("Sheriff", ("Susan", "Harrison", 49))
])
([("Sharon", "Stevens", 38), ("Christine", "Washington", 47)])
newDirectory :: GovDirectory Person
newDirectory = personFromTuple <$> oldDirectory
CONCLUSIONВыводы
NowТеперь thatвы youзнаете knowо aboutфункторах, functors,нужно it'sвремя, timeчтобы toпонять deepenэти yourтипы understandingструктур. ofДвигаемся theseк kindsчасти of2, structures.где Soмы moveобсудим ontoприменение part 2 where we'll discuss applicative functors. If you're dying to try out some of these examples but have never tried Haskell before, download our Beginners Checklist to learn how!функторов.