На пути изучения Haskell стоит много абстракций, без понимания которых нет смысла двигаться дальше. Один из таких рубежей — аппликативный функтор.
Разберем пример: напишем простой валидатор и посмотрим каким образом пригодится Applicative.
Определим пользователя:
type Name = String
type Email = String
data User = User Name Email
deriving (Show)
Попробуем стандартный Either
Функции валидации:
validateName :: Name -> Either [String] Name
validateName "" = Left ["name cannot be empty"]
validateName s = pure s
validateEmail :: Email -> Either [String] Email
validateEmail "" = Left ["email cannot be empty"]
validateEmail s = pure s
Тип Either является и функтором, и аппликативным функтором, воспользуемся этим, чтобы написать функцию которая или создает пользователя или нет.
validateUser :: Name -> Email -> Either [String] User
validateUser n e =
User <$> validateName n
<*> validateEmail e
-- >>> validateUser "John" "john@email.com"
-- Right (User "John" "john@email.com")
-- >>> validateUser "" ""
-- Left ["name cannot be empty"]
Работает, но хотелось бы, чтобы валидатор собирал все ошибки. Вот как должен выглядеть результат последнего примера:
-- Left ["name cannot be empty","email cannot be empty"]
Новый тип Validator
Either не подходит для нашей задачи. Но он нам пригодится:
newtype Validator a = Validator
{ runValidator :: Either [String] a
}
Да, тип ошибки: список строк, подходит для нашей задачи. А как же универсальность?
newtype Validator e a = Validator
{ runValidator :: Either e a
}
Сейчас нужно определить экземпляры классов Functor и Applicative для валидатора. Нас устроит реализация Functor для Either:
instance Functor (Validator e) where
fmap f = Validator . (f <$>) . runValidator
Нам нужно будет объединять ошибки для этого подойдет (<>) из класса типов Semigroup. Учтем это:
instance Semigroup e => Applicative (Validator e) where
pure = Validator . pure
Validator (Left e1) <*> Validator (Left e2) =
Validator $ Left $ e1 <> e2
Validator eF <*> Validator eA =
Validator $ ($) <$> eF <*> eA
Пригодится вспомогательная функция:
throwError :: e -> Validator e a
throwError = Validator . Left
Перепишем функции валидации:
validateName :: Name -> Validator [String] Name
validateName "" = throwError ["name cannot be empty"]
validateName s = pure s
validateEmail :: Email -> Validator [String] Email
validateEmail "" = throwError ["email cannot be empty"]
validateEmail s = pure s
validateUser :: Name -> Email -> Either [String] User
validateUser n e =
runValidator $
User <$> validateName n
<*> validateEmail e
-- >>> validateUser "John" "john@email.com"
-- Right (User "John" "john@email.com")
-- >>> validateUser "" ""
-- Left ["name cannot be empty","email cannot be empty"]
Такой результат нас устраивает. И обошлись совсем без монад.