Is there a way to preserve the abstraction of a semigroup or monoid, but still get a lazy IO?
Several, but there are drawbacks. The udnerlying task for our instances is that the generic instance for Applicative will look like
instance Semigroup a => Semigroup (SomeApplicative a) where x <> y = (<>) <$> x <*> y
We are in power here (<*>) , and usually the second argument y will be at least in WHNF. For example, in the Maybe implementation, the first line will work fine, and the second line will be error :
liftA2 (<>) Just (First 10) <> Just (error "never shown") liftA2 (<>) Just (First 10) <> error "fire!"
IO (<*>) is implemented in terms of ap , so the second action will always be performed before applying <> .
A First option is possible with ExceptT or similar, essentially any data type that has Left k >>= _ = Left k so that we can stop the calculation at this point. Although ExceptT is for exceptions, it may work well for your use case. Alternatively, one of the Alternative transformers ( MaybeT , ExceptT ) along with <|> instead of <> may be sufficient.
An almost completely lazy type of I / O is also possible, but it must be handled with care:
import Control.Applicative (liftA2) import System.IO.Unsafe (unsafeInterleaveIO) newtype LazyIO a = LazyIO { runLazyIO :: IO a } instance Functor LazyIO where fmap f = LazyIO . fmap f . runLazyIO instance Applicative LazyIO where pure = LazyIO . pure f <*> x = LazyIO $ do f' <- unsafeInterleaveIO (runLazyIO f) x' <- unsafeInterleaveIO (runLazyIO x) return $ f' x' instance Monad LazyIO where return = pure f >>= k = LazyIO $ runLazyIO f >>= runLazyIO . k instance Semigroup a => Semigroup (LazyIO a) where (<>) = liftA2 (<>) instance Monoid a => Monoid (LazyIO a) where mempty = pure mempty mappend = liftA2 mappend
unsafeInterleaveIO allows you to use the behavior you want (and is used in getContents and other lazy IO Prelude functions), but it should be used with caution. The IO order of operations is completely disabled at this point. Only when we check the values ββwill we start the original IO :
ghci> :module +Data.Monoid Control.Monad ghci> let example = fmap (First . Just) . LazyIO . putStrLn $ "example" ghci> runLazyIO $ fmap mconcat $ replicateM 100 example First {getFirst = example Just ()}
Please note that we got only one example example in the output, but in a completely random place, since putStrLn "example" and print result received alternation, since
print (First x) = putStrLn (show (First x)) = putStrLn ("First {getFirst = " ++ show x ++ "}")
and show x will finally put IO to get x in action. The action will be called only once if we use the result several times:
ghci> :module +Data.Monoid Control.Monad ghci> let example = fmap (First . Just) . LazyIO . putStrLn $ "example" ghci> result <- runLazyIO $ fmap mconcat $ replicateM 100 example ghci> result First {getFirst = example Just ()} ghci> result First {getFirst = Just ()}
You can write a finalizeLazyIO function that either evaluate or seq x , though:
finalizeLazyIO :: LazyIO a -> IO a finalizeLazyIO k = do x <- runLazyIO k x `seq` return x
If you want to publish a module with these functions, I would recommend exporting only a constructor of the type LazyIO , liftIO :: IO a -> LazyIO a and finalizeLazyIO .