Storage IO lazy when adding

I might have had the false impression that Haskell is lazier than it is, but I wonder if there is a way to get the best of both worlds ...

Data.Monoid and Data.Semigroup define two variants of First . The monoidal version models the leftmost non-empty value, while the semigroup version simply models the leftmost value.

This works fine for pure values, but takes into account unclean values:

 x = putStrLn "x" >> return 42 y = putStrLn "y" >> return 1337 

Both of these values ​​are of type Num a => IO a . IO a is an instance of Semigroup when a :

 instance Semigroup a => Semigroup (IO a) -- Defined in `Data.Orphans' 

This means that you can combine the two values ​​of IO (First a) :

 Prelude Data.Semigroup Data.Orphans> fmap First x <> fmap First y x y First {getFirst = 42} 

As we can see, both x and y create their respective side effects, although y never required.

The same applies to Data.Monoid :

 Prelude Data.Monoid> fmap (First . Just) x <> fmap (First . Just) y x y First {getFirst = Just 42} 

I think I understand why this happens, given that both instances of Semigroup and Monoid use liftA2 , which seems to ultimately be based on IO bind, which is strict as I understand it.

If I get along with the First abstraction, however, I can get a more lazy score:

 first x _ = x mfirst xy = do x' <- x case x' of (Just _) -> return x' Nothing -> y 

Using both of these ignoring y :

 Prelude> first xy x 42 Prelude> mfirst (fmap Just x) (fmap Just y) x Just 42 

In both cases, y not printed.

My question is:

Can I get the best of both worlds? Is there a way I can keep the abstraction of a semigroup or monoid, but still get a lazy IO?

Is there, for example, some kind of LazyIO container that I can wrap First values ​​with, so that I get the lazy IO that I would like to have?

The actual scenario that I need is that I would like to request a list of IO resources with priority for the data and use the first one that gives me a useful answer. However, I do not want to make redundant requests (for performance reasons).

+4
haskell lazy-evaluation monoids semigroup
source share
2 answers

An Alternative instance for MaybeT monad transformer returns the first successful result and does not perform the rest of the operations. In combination with the asum function, we can write something like:

 import Data.Foldable (asum) import Control.Applicative import Control.Monad.Trans.Maybe action :: Char -> IO Char action c = putChar c *> return c main :: IO () main = do result <- runMaybeT $ asum $ [ empty , MaybeT $ action 'x' *> return Nothing , liftIO $ action 'v' , liftIO $ action 'z' ] print result 

where the final action 'z' will not be executed.

We can also write a newtype shell with a Monoid instance that mimics Alternative :

 newtype FirstIO a = FirstIO (MaybeT IO a) firstIO :: IO (Maybe a) -> FirstIO a firstIO ioma = FirstIO (MaybeT ioma) getFirstIO :: FirstIO a -> IO (Maybe a) getFirstIO (FirstIO (MaybeT ioma)) = ioma instance Monoid (FirstIO a) where mempty = FirstIO empty FirstIO m1 `mappend` FirstIO m2 = FirstIO $ m1 <|> m2 

The relationship between Alternative and Monoid explained in this other SO question .

+1
source share

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 .

+1
source share

All Articles