Как стать автором
Поиск
Написать публикацию
Обновить

Валидатор на Haskell и причем здесь Applicative

Время на прочтение2 мин
Количество просмотров154

На пути изучения 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"]

Такой результат нас устраивает. И обошлись совсем без монад.

Теги:
Хабы:
0
Комментарии0

Публикации

Ближайшие события