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

INTRODUCTIONВведение TOв FUNCTORSфункторы

WithС thisэтой ideaидеи inмы mind,можем weначать canразбирать startфункторы. understandingПервое functors.и Firstглавное: andФунктор foremost,это Functorкласс isтипа a typeclass inв Haskell. InДля orderтипов forкоторые aявляются dataэкземплярами typeфункторных toклассов beтипа, anони instanceдолжны ofреализовывать theпростую Functorфункцию: typeclass, it must implement a single function: fmap.fmap.

fmap :: (a -> b) -> f a -> f b

TheФункция fmap functionпринимает takesдва twoввода. inputs.Первый First,- itтребует demandsфункцию aдля functionвдух betweenтипов twoданных. dataВторой types.параметр The- secondхранилище parameterпервоо isтипа. someВывод container- ofхранилище theвторого firstтипа. type.Теперь Theвзглянем outputна thenнесколько isразличных aэкземпляров containerфункторов ofдля theзнакомых secondтипов. type.Для Nowсписков, let'sfmap lookпросто atопределяется aкак fewбазовая differentфункция Functor instances for some familiar types. For lists, fmap is simply defined as the basic map function::

instance Functor [] where
  fmap = map

InНа fact,самом деле, fmap isэто aобобщение generalizationсоответствия. ofНапример, mapping.тип Forданных example,Map theтак Mapже dataфунктор. typeОн isиспользует alsoсвою aсобственную functor.функцию Itmap usesдля itsfmap. ownФункторы mapпросто functionберут forэту fmap.идею Functorsпреобразования simplyвсех takeниже thisлежащих ideaзначений ofи transformingприменяют allих underlyingк valuesдругим andтипам. applyС itэтим, toдавайте otherвзглянем types.на WithMaybe thisкак inна mind, let's observe how Maybe is a functor:функтор:

instance Functor Maybe where
  fmap _ Nothing = Nothing
  fmap f (Just a) = Just (f a)

ThisВыглядит looksдовольно aпохоже lotна likeнашу ourфункцию originalconvertTuple. convertTupleЕсли function!у Ifнас weнет haveзначения noна valueпервом inместе, theтогда firstрезультат place,Nothing. thenЕсли theимеется resultзначение, isтогда Nothing.просто Ifприменяется weфункция doк haveзначение aи value,превращает simplyеё applyв theJust. functionТип toданных theEither value,может andбыть rewrapтипом itMaybe inс Just.дополнительной Theинформацией Eitherпо dataкакой typeпричине. canОн beимеет seenсхожее as a Maybe type with more information about how it failed. It has similar behavior:поведение:

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 second parameter of an Either value is changed by fmap. Based on these examples, we 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!