Search code examples
haskelllazy-evaluation

Cannot call IO () function even with bang patterns


I have some library which have a function to make plot using gnuplot library:

import Graphics.Gnuplot.Simple

drawMap :: [(Int, Int)] -> IO ()                                                   
drawMap m = do                                                                     
  !a <- plotList [] m                                                              
  print a 

I call this function from main like this:

main = do                                                                          
  !a <- drawMap [(1,2),(3,5)]                                                      
  print a     

I build project with stack and I've tried -O2 and -O0 optimization, but plot never works (print a functions are always called successfully and print ()). How can I force plotting and why it doesn't work, with library, but works if I just call plotList from main?

UPD.

Using strict application in main and in drawMap by $! doesn't work naither:

drawMap :: [(Int, Int)] -> IO ()                                                   
drawMap m = plotList [] $! m                                                                  

main = do                                                                                                                                                 
  drawMap $! [(1,2),(3,5)] 

UPD 2 Some minimal example:

This does not work for me:

import Graphics.Gnuplot.Simple                                                 
                                                                               
main = plotList [] ([(1,2),(3,5)] :: [(Int,Int)])     

But this works:

{-# LANGUAGE BangPatterns #-}                                                  
                                                                               
import Graphics.Gnuplot.Simple                                                 
                                                                               
main = do                                                                      
  !a <- plotList [] ([(1,2),(3,5)] :: [(Int,Int)])                             
  print a  

But code from my question does not work even with bang patterns/strict application, if drawMap is in other module than main.


Solution

  • Strictness is a red herring. The library is not doing concurrency correctly. Some source diving shows this:

    runGnuplot ::
       Graph.C graph =>
       [Attribute] -> String -> Plot.T graph -> IO ()
    runGnuplot attrs cmd (Plot.Cons mp) =
       void $ Cmd.asyncIfInteractive (interactiveTerm attrs) $ Cmd.run $ \dir ->
          let files = MR.runReader (MS.evalStateT mp 0) dir
          in  (map attrToProg attrs ++
               [cmd ++ " " ++
                extractRanges attrs ++ " " ++
                commaConcat (plotFileStatements files)],
               files)
    
    interactiveTerm :: [Attribute] -> Bool
    interactiveTerm =
       all $ \attr ->
          case attr of
             Terminal term -> Terminal.interactive term
             PNG _ -> False
             EPS _ -> False
             _ -> True
    
    asyncIfInteractive :: Bool -> IO ExitCode -> IO ExitCode
    asyncIfInteractive interactive act =
       if interactive
         then fmap (const ExitSuccess) $ forkIO $ void act
         else act
    

    In particular, when the list of attributes is empty, it will be considered "interactive", and the IO action will be forked into its own thread. The semantics of Haskell programs is that they exit when the main thread exits, so this is a race condition: will main exit first, or will the forked thread call out to gnuplot first?

    The right thing to do here is for runGnuplot to provide the user with an IO action that waits for the forked thread to finish that they can call from their main thread (for example by allocating an MVar, writing to it in the forked thread, and reading from it in the returned action). The simple wrong thing to do is to toss a threadDelay into your program:

    import Graphics.Gnuplot.Simple
    import Control.Concurrent
    
    main = do
        plotList [] ([(1,2),(3,5)] :: [(Int,Int)])
        threadDelay 1000000
    

    It's still a race condition, but now the forked thread has at least one second to make it to its gnuplot call before the program is forcibly destroyed -- for a computer, that is nearly an eternity.