Функторы
Добро пожаловать в нашу серию статей. Монады одна из тех идей, которая кажется причиной множества страхов и мучений среди множества людей пробоющих 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 (String, String, Int). However, another part could use the type GovDirectory Person. We can define the following Functor instance for GovDirectory by 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 for 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! We can just use fmap in conjunction with our transformation function, 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 of 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!