Question

type PromptSegment = IO (Maybe String)

instance Monoid a => Monoid (IO a) where
  mempty = return mempty
  mappend = liftA2 (<>)

This behaves exactly how I want for my purposes.

For example:

ghci> let a = return $ Just "hello" :: IO Maybe String
ghci> let b = return $ Just " world" :: IO Maybe String
ghci> let c = return $ Nothing :: IO Maybe String
ghci> a `mappend` b `mappend` c
Just "hello world"

However, I'm pretty aware that maybe, for a different Monoid a => Monoid (IO a) instance other than a PromptSegment, maybe I wouldn't want mappend to behave the same way?? I feel like there's better way to do this than creating the orphan monoid instance above.

Current API design

currentDirectory :: PromptSegment
currentDirectory = Just <$> getCurrentDirectory

main :: IO ()
main = buildMainPrompt
         [ bold . fgColor skyBlue <$> currentDirectory
         ,  (fgColor deepSkyBlue3 . underline . bold <$> gitCurrentBranch)
           <> (fgColor defaultDarkGreen . bold <$> gitRepositorySymbol "±")
           <> gitStatusSegment
         ]
         (fgColor red0 . bold <$> makePromptSegment " ➢ ")
         (fgColor slateBlue0 . bold <$> makePromptSegment " λ» ")

gitStatusSegment :: PromptSegment
gitStatusSegment =
  let unstagedSymbol = fgColor gold1 <$> gitUnstagedSymbol "✚"
      stagedSymbol   = fgColor orange <$> gitStagedSymbol "✎"
      pushSymbol     = fgColor red1 . bold <$> gitPushSymbol "↑"
  in prependSpace <$> unstagedSymbol <> stagedSymbol <> pushSymbol

This is, of course, a work in progress, but the aim of the program is to create a DSL for defining cli prompt themes (starting with ZSH).This last code sample is mostly to serve the purpose of providing context to the question for why I would want to make IO (Maybe String) a Monoid. I'm wide open to harsh criticism - especially since this is my first attempt at writing a 'real' program in Haskell.

Was it helpful?

Solution

There is are reasons behind warning again orphan instances. In particular: If we start using orphan instances, modules can become mutually incompatible: What if two modules define two different instances Monoid (IO a)? There is no good way to tell which one should be preferred.

Furthermore using orphan instances often leads to bad program design. If you add an orphan instance to a data type, you're adding some functionality to the data type. And sooner or later you'll also need to modify the functionality, which isn't (by design) possible with Haskell type classes. At this point you either have to refactor your program or start bending the rules add mess up your design. (This is different from the OO world where subclasses are meant to modify some of the functionality of their parents.)

Therefore I'd strongly suggest to create a new data type for your use case. You'll pay a small one-time price for creating aliases for functions you'll use in your project (most likely in a dedicated module), like

import qualified System.Directory as D
getCurrentDirectory :: MyDSL FilePath
getCurrentDirectory = --  something wraping D.getCurrentDirectory 

or more generally define instance MonadIO MyDSL and then

getCurrentDirectory :: MonadIO FilePath
getCurrentDirectory = liftIO D.getCurrentDirectory

But then your implementation will be completely independent and as your project evolves, nothing will limit you to make the internals of MyDSL more complex or add functionality. Moreover, it'll be safe to use as a library in other projects, without the risk of getting conflicting instances.

Such separation is even more important when you're creating a DSL, because in this case you strongly want to separate the language from its internal implementation. Using an existing data type for a DSL is very likely to cause problems, either when implementing it or due to improper usage by its users.

Licensed under: CC-BY-SA with attribution
scroll top