Если вы видите что-то необычное, просто сообщите мне. Skip to main content

Функторы

Добро пожаловать в нашу серию статей. Монады одна из тех идей, которая кажется причиной множества страхов и мучений среди множества людей пробоющих 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)

NoteОтметим, theчто firstпараметр typeпервого parameterтипа ofэтого thisобъекта instanceисправлен. isТолько fixed.второй Onlyпараметр theзначения secondEither parameterизменен ofс anпомощью Eitherfmap. valueОсновываясь isна changedэтих byпримерах, fmap.мы Basedможем onувидеть theseкак examples,переписать weconvertTuple can see how to rewrite convertTuple to be more generic:обощеннее:

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!