Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#100] Add Validation data type to Extra modules #106

Merged
merged 5 commits into from Oct 16, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,8 @@
Reexport `Bitraversable` related stuff from `base`.
* [#97](https://github.com/kowainik/relude/issues/97):
Add `(&&^)` and `(||^)` operators.
* [#100](https://github.com/kowainik/relude/issues/100):
Add `Validation` data type to Extra modules.

0.3.0
=====
Expand Down
1 change: 1 addition & 0 deletions relude.cabal
Expand Up @@ -120,6 +120,7 @@ library
Relude.Extra.Newtype
Relude.Extra.Tuple
Relude.Extra.Type
Relude.Extra.Validation
Relude.Unsafe

ghc-options: -Wall
Expand Down
1 change: 1 addition & 0 deletions src/Relude.hs
Expand Up @@ -89,6 +89,7 @@ every module in your package by modifying your "Prelude" file:
@newtype@.
* __"Relude.Extra.Tuple"__: functions for working with tuples.
* __"Relude.Extra.Type"__: functions for inspecting and working with types.
* __"Relude.Extra.Validation"__: 'Validation' data type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for updating the documentation! ❤️

* __"Relude.Unsafe"__: unsafe partial functions (produce 'error') for lists and
'Maybe'.
-}
Expand Down
180 changes: 180 additions & 0 deletions src/Relude/Extra/Validation.hs
@@ -0,0 +1,180 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE InstanceSigs #-}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading this module I'm now even more sure that -XInstanceSigs should be enabled by default!

{-# LANGUAGE LambdaCase #-}

{- |
Copyright: (c) 2014 Chris Allen, Edward Kmett
(c) 2018 Kowainik
License: MIT
Maintainer: Kowainik <xrom.xkov@gmail.com>

Monoidal 'Validation' sibling to 'Either'.
-}

module Relude.Extra.Validation
( Validation(..)
, validationToEither
, eitherToValidation
) where

import Relude

-- $setup
-- >>> :set -XTypeApplications -XOverloadedStrings

-- | 'Validation' is 'Either' with a Left that is a 'Monoid'
data Validation e a
= Failure e
| Success a
deriving (Eq, Ord, Show)

instance Functor (Validation e) where
fmap :: (a -> b) -> Validation e a -> Validation e b
fmap _ (Failure e) = Failure e
fmap f (Success a) = Success (f a)

(<$) :: a -> Validation e b -> Validation e a
x <$ Success _ = Success x
_ <$ Failure e = Failure e

{-# INLINE fmap #-}
{-# INLINE (<$) #-}

-- | Examples:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's a good idea to write this as ==== __Examples__ to make them expandable 🤔
Not sure how it will look like.

Also, IMO, for multiline haddock comments it's better to use: the following style

{- |
-}

so you won't need to add -- at the beginning of each line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed expandable formatting works, but looks like this in Firefox and Chrome:

screenshot from 2018-10-16 06-41-50

It currently looks like this:

screenshot from 2018-10-16 07-13-33

WDYT? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that non-expandable is better 🙂

--
-- >>> let fa = Success (*3) :: Validation Text (Int -> Int)
-- >>> let ga = Success (*4) :: Validation Text (Int -> Int)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example purposes it's better to use [Text] instead of just Text. It's not a good idea to use Text as a monoidal error, so let's not show non-idiomatic examples in our documentation examples.

-- >>> let a = Success 1 :: Validation Text Int
-- >>> let b = Success 7 :: Validation Text Int
-- >>> let c = Failure "Not correct " :: Validation Text Int
-- >>> let d = Failure "Not correct either" :: Validation Text Int
--
-- >>> fa <*> b
-- Success 21
--
-- >>> fa <*> c
-- Failure "Not correct "
--
-- >>> c *> d *> b
-- Failure "Not correct Not correct either"
--
-- >>> liftA2 (+) a b
-- Success 8
--
-- >>> liftA2 (+) a c
-- Failure "Not correct "

instance Semigroup e => Applicative (Validation e) where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be really great to add couple doctest tests to this instance to show different cases 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good 👍

pure :: a -> Validation e a
pure = Success

liftA2 :: (a -> b -> c) -> Validation e a -> Validation e b -> Validation e c
liftA2 _ (Failure el) (Failure er) = Failure (el <> er)
liftA2 _ (Failure e) (Success _) = Failure e
liftA2 _ (Success _) (Failure e) = Failure e
liftA2 f (Success a) (Success b) = Success (f a b)

(<*>) :: Validation e (a -> b) -> Validation e a -> Validation e b
Failure e <*> b = Failure $ case b of
Failure e' -> e <> e'
Success _ -> e
Success _ <*> Failure e = Failure e
Success f <*> Success a = Success (f a)


(*>) :: Validation e a -> Validation e b -> Validation e b
(*>) = liftA2 (flip const)

(<*) :: Validation e a -> Validation e b -> Validation e a
(<*) = liftA2 const
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the default definitions of <* and *>. I wonder, if we can write them more efficiently if we do pattern-matching explicitly? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are both defined based on liftA2, which is a custom implementation and actually uses pattern-matching explicitly, so I thought perhaps it was best to just leave these methods for readibility's sake. I can change it anyway, WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the default implementation here as well 👍


{-# INLINE pure #-}
{-# INLINE liftA2 #-}
{-# INLINE (<*>) #-}
{-# INLINE (*>) #-}
{-# INLINE (<*) #-}

instance (Semigroup e, Monoid e) => Alternative (Validation e) where
empty :: Validation e a
empty = Failure mempty

(<|>) :: Validation e a -> Validation e a -> Validation e a
s@Success{} <|> _ = s
_ <|> s@Success{} = s
Failure e <|> Failure e' = Failure (e <> e')

{-# INLINE empty #-}
{-# INLINE (<|>) #-}

instance Foldable (Validation e) where
fold :: Monoid m => Validation e m -> m
fold (Success a) = a
fold (Failure _) = mempty

foldMap :: Monoid m => (a -> m) -> Validation e a -> m
foldMap f = foldr (mappend . f) mempty
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think foldMap can be written more efficiently more via explicit pattern-matching

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of the same thing as with <* but in terms of foldr. I can change it anyway, WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, for <* and *> we're kinda hope for the inlining and optimizations and implementing <* as liftA2 const is not a big deal. But for foldMap optimizations might not trigger because the implementation is more difficult. So it's better to pattern-match here explicitly as well.


foldr :: (a -> b -> b) -> b -> Validation e a -> b
foldr f x (Success a) = f a x
foldr _ x (Failure _) = x

{-# INLINE fold #-}
{-# INLINE foldMap #-}
{-# INLINE foldr #-}

instance Traversable (Validation e) where
traverse :: Applicative f => (a -> f b) -> Validation e a -> f (Validation e b)
traverse f (Success a) = Success <$> f a
traverse _ (Failure e) = pure (Failure e)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also implement sequenceA here?


{-# INLINE traverse #-}

instance Bifunctor Validation where
bimap :: (e -> d) -> (a -> b) -> Validation e a -> Validation d b
bimap f _ (Failure e) = Failure (f e)
bimap _ g (Success a) = Success (g a)

first :: (e -> d) -> Validation e a -> Validation d a
first f (Failure e) = Failure (f e)
first _ (Success a) = Success a

second :: (a -> b) -> Validation e a -> Validation e b
second _ (Failure e) = Failure e
second g (Success a) = Success (g a)

{-# INLINE bimap #-}
{-# INLINE first #-}
{-# INLINE second #-}

#if MIN_VERSION_base(4,10,0)
instance Bifoldable Validation where
bifoldMap :: Monoid m => (e -> m) -> (a -> m) -> Validation e a -> m
bifoldMap f _ (Failure e) = f e
bifoldMap _ g (Success a) = g a

{-# INLINE bifoldMap #-}

instance Bitraversable Validation where
bitraverse :: Applicative f
=> (e -> f d) -> (a -> f b) -> Validation e a -> f (Validation d b)
bitraverse f _ (Failure e) = Failure <$> f e
bitraverse _ g (Success a) = Success <$> g a

{-# INLINE bitraverse #-}
#endif

-- | Transform a 'Validation' into an 'Either'.
validationToEither :: Validation e a -> Either e a
validationToEither = \case
Failure e -> Left e
Success a -> Right a

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line here is redundant 🙂

{-# INLINE validationToEither #-}

-- | Transform an 'Either' into a 'Validation'.
eitherToValidation :: Either e a -> Validation e a
eitherToValidation = \case
Left e -> Failure e
Right a -> Success a

{-# INLINE eitherToValidation #-}