Search code examples
haskellmonadssmtio-monadsbv

How to avoid the IO monad when solving arithmetic problems in SBV


I am trying to solve arithmetic problems with SBV.

For example

solution :: SymbolicT IO ()
solution =  do 
    [x, y] <- sFloats ["x", "y"]
    constrain $ x + y .<= 2
Main> s1 = sat solution
Main> s2 = isSatisfiable solution
Main> s1
Satisfiable. Model:
 x = -1.2030502e-17 :: Float
 z = -2.2888208e-37 :: Float
Main> :t s1
s1 :: IO SatResult
Main> s2
True
Main> :t s2
s2 :: IO Bool

While I can do useful things, it is easier for me to work with the pure value (SatResult or Bool) and not with the IO monad.

According to the documentation

sat :: Provable a => a -> IO SatResult
constrain :: SolverContext m => SBool -> m ()
sFloats :: [String] -> Symbolic [SFloat] 
type Symbolic = SymbolicT IO

Given the type of functions I use, I understand why I always get to the IO monad.

But looking in the generalized versions of the functions for example sFloats.

sFloats :: MonadSymbolic m => [String] -> m [SFloat] 

Depending on type of the function, I can work with a different monad than IO. This gives me hope that we will reach a more useful monad, the Identity monad for example.

Unfortunately looking at the examples always solves the problems within the IO monad, so I couldn't find any examples that would work for me.Besides that I don't have much experience working with monads.

Finally My question is:

Is there any way to avoid the IO monad when solving such a problem with SBV?

Thanks in advance


Solution

  • SBV calls out to the SMT solver of your choice (most likely z3, but others are available too), and presents the results back to you. This means that it performs IO under the hood, and thus you cannot be outside the IO monad. You can create custom monads using MonadSymbolic, but that will not get you out of the IO monad: Since the call to the SMT solver does IO you'll always be in IO.

    (And I'd strongly caution against uses of unsafePerformIO as suggested in one of the comments. This is really a bad idea; and you can find lots more information on this elsewhere why you shouldn't do so.)

    Note that this is no different than any other IO based computation in Haskell: You perform the IO "in-the-wrapper," but once you get your results, you can do whatever you'd like to do with them in a "pure" environment.

    Here's a simple example:

    import Data.SBV
    import Data.SBV.Control
    
    example :: IO ()
    example =  runSMT $ do
        [x, y] <- sFloats ["x", "y"]
        constrain $ x + y .<= 2
    
        query $ do cs <- checkSat
                   case cs of
                     Unsat -> io $ putStrLn "Unsatisfiable"
                     Sat   -> do xv <- getValue x
                                 yv <- getValue y
    
                                 let result = use xv yv
    
                                 io $ putStrLn $ "Result: " ++ show result
    
                     _   -> error $ "Solver said: " ++ show cs
    
    -- Use the results from the solver, in a purely functional way
    use :: Float -> Float -> Float
    use x y = x + y
    

    Now you can say:

    *Main> example
    Result: -Infinity
    

    The function example has type IO (), because it does involve calling out to the solver and getting the results. However, once you extract those results (via calls to getValue), you can pass them to the function use which has a very simple purely functional type. So, you keep the "wrapper" in the monad, but actual processing, use-of-the values, etc., remain in the pure world.

    Alternatively, you can also extract the values and continue from there:

    import Data.SBV
    import Data.SBV.Control
    
    example :: IO (Maybe (Float, Float))
    example =  runSMT $ do
        [x, y] <- sFloats ["x", "y"]
        constrain $ x + y .<= 2
    
        query $ do cs <- checkSat
                   case cs of
                     Unsat -> pure Nothing
                     Sat   -> do xv <- getValue x
                                 yv <- getValue y
    
                                 pure $ Just (xv, yv)
    
                     _   -> error $ "Solver said: " ++ show cs
    

    Now you can say:

    *Main> Just (a, b) <- example
    *Main> a
    -Infinity
    *Main> b
    4.0302105e-21
    

    Long story short: Don't avoid the IO monad. It's there for a very good reason. Get into it, get your results out, and then the rest of your program can remain purely functional, or whatever other monad you might find yourself in.

    Note that none of this is really SBV specific. This is the usual Haskell paradigm of how to use functions with side-effects. (For instance, anytime you use readFile to read the contents of a file to process it further.) Do not try to "get rid of the IO." Instead, simply work with it.