3
\$\begingroup\$

I'm currently learning Haskell and are starting to understand applicatives.

I wanted an Either Applicative where the left side is a monoid, so I created this as a learning exercise:

module EitherLeftMonoid where

data Error = InvalidName | UnderAge deriving (Eq, Show)

data Person = Person { name :: String, age :: Int } deriving (Eq, Ord, Show)

data EitherLeftMonoid a b = Lefty a
                          | Righty b
                          deriving (Eq, Show)

instance Monoid a => Functor (EitherLeftMonoid a) where
  fmap _ (Lefty a)  = Lefty a
  fmap f (Righty b) = Righty (f b)

instance Monoid a => Applicative (EitherLeftMonoid a) where
  pure                  = Righty
  Lefty a <*> Lefty a'  = Lefty (a `mappend` a')
  Lefty a <*> _         = Lefty a
  _ <*> (Lefty a)       = Lefty a
  Righty f <*> Righty x = Righty (f x)

mkName :: String -> EitherLeftMonoid [Error] String
mkName "" = Lefty [InvalidName]
mkName s  = Righty s

mkAge :: Int -> EitherLeftMonoid [Error] Int
mkAge n | n < 13    = Lefty [UnderAge]
        | otherwise = Righty n

mkPerson :: String -> Int -> EitherLeftMonoid [Error] Person
mkPerson n a = Person <$> mkName n <*> mkAge a

Output:

Prelude> mkPerson "" 12
Lefty [InvalidName,UnderAge]
Prelude> mkPerson "" 13
Lefty [InvalidName]
Prelude> mkPerson "Gerd" 13
Righty (Person {name = "Gerd", age = 13})

What could I have done different to make the code better?

\$\endgroup\$
1
\$\begingroup\$

Your Functor instance doesn't need a to be a Monoid. We can remove that constraint to make fmap on EitherLeftMonoid more general.

Your Applicative instance could be made a little bit more readable if you provide a fromLefty :: Monoid a => EitherLeftMonoid a b -> a function:

instance Functor (EitherLeftMonoid a) where
  fmap _ (Lefty a)  = Lefty a
  fmap f (Righty b) = Righty (f b)

instance Monoid a => Applicative (EitherLeftMonoid a) where
  pure                  = Righty
  Righty f <*> Righty x = Righty (f x)
  x        <*> y        = Lefty $ fromLefty x `mappend` fromLefty y

fromLefty :: Monoid a => EitherLeftMonoid a b -> a
fromLefty (Left x) = x
fromLefty _        = mempty

With fromLefty we can take advantage of mappend mempty x = x = mappend x mempty.


In this case everything is in a single module and everything gets exported. This enables a user to use the Person constructor immediately. If you want to prevent that you need to make sure that only the type, but neither the records nor the data constructor get exported. Otherwise mkPerson can get circumvented.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.