packages feed

mathlink-1.0.0.0: src/Foreign/MathLink.hs

{-# LANGUAGE MultiParamTypeClasses
           , FlexibleContexts
           , DeriveDataTypeable
           #-}

-- | A Haskell interface to /Mathematica/'s /MathLink/.
module Foreign.MathLink ( -- * Basic usage

                          -- $usage

                          -- * Types
                          Function(..)

                          -- * Running /MathLink/
                        , runMathLink
                        , runMathLinkWithArgs

                          -- * Data marshaling
                        , get
                        , put

                          -- * Accessing /MathLink/ state
                        , getLink
                        , checkAbort
                        , checkDone

                          -- * Evaluation
                        , evaluate
                        , evaluateAndWait
                        ) where

import Data.Int
import Data.IORef
import Data.Typeable
import Data.Array.Unboxed

import qualified Data.IntMap as IM ( IntMap
                                   , toList
                                   , fromList
                                   , lookup
                                   )

import Control.Monad.Reader
import Control.Monad.Error

import Control.Exception hiding ( evaluate
                                )

import System.IO ( hPutStrLn
                 , stderr
                 )

import System.Environment ( getArgs
                          )

import Foreign.MathLink.Expressible
import Foreign.MathLink.IO

data MLError = MLError
             | MLErrorMsg String
             | MLException SomeException
             | MLErrors [MLError]
               deriving (Typeable)

instance Show MLError where
    show MLError = "MLError"
    show (MLErrorMsg s) = "MLErrorMsg: " ++ s
    show (MLException e) = "MLException: " ++ (show e)
    show (MLErrors errs) = "MLErrors: " ++ show errs

instance Error MLError where
    noMsg = MLError
    strMsg s = MLErrorMsg s

data MLConfig = MLConfig { functionTable :: IM.IntMap Function
                         }

-- | Encapsulates a description of a function callable from /Mathematica/
data Function = 
    Function { -- | A string representing the /Mathematica/ pattern
               -- whose match should result in a function call to 
               -- the specified Haskell function. Analogous to the 
               -- @:Pattern:@ directive in an input to 
               -- /Mathematica/'s @mprep@ utility.
               callPattern :: String
               -- | A string representing the /Mathematica/ pattern
               -- for the argument that will be marshaled from
               -- /Mathematica/ to Haskell. Pattern variables
               -- appearing here are bound in the 'callPattern'
               -- match. Analogous to the @:Arguments:@ directive
               -- in an input to /Mathematica/'s @mprep@ utility.
             , argumentPattern :: String
               -- | The Haskell function to be invoked.
             , function :: IO ()
             }

instance Show Function where
    show fn = "Function { callPattern = " ++ 
              (show $ callPattern fn) ++ 
              ", argumentPattern = " ++
              (show $ argumentPattern fn) ++ " }"

type ML a = ErrorT MLError (ReaderT MLConfig IO) a

getConfig :: ML MLConfig
getConfig = ask

-- | Runs /MathLink/, exposing the given list of functions.
runMathLink :: [Function] -> IO ()
runMathLink functions = do 
  args <- getArgs
  runMathLinkWithArgs args functions

-- | Like 'runMathLink', but explicitly requires the command line
--   arguments to be passed to /MathLink/.
runMathLinkWithArgs :: [String] 
                    -> [Function] -> IO ()
runMathLinkWithArgs args functions = do
  initializeMathLink $ unwords args
  er <- (runReaderT (runErrorT runLoop) config)
  finalizeMathLink
  case er of
    Left err -> do hPutStrLn stderr $ show err
                   return ()
    Right () -> return ()
  where config = MLConfig { functionTable = 
                                IM.fromList $ zip [0..] functions
                          }
                  
runLoop :: ML ()
runLoop = do
  installFunctionTable
  processPackets

processPackets :: ML ()
processPackets = do
  pkt <- answer
  case pkt of
    ResumePacketCode -> do
      liftIO $ refuseToBeAFrontEnd
      processPackets
    _ -> return ()
          
answer :: ML PacketCode
answer = do
  dn <- liftIO $ checkDone
  if dn then do
      liftIO clearAbort
      return $ mkPacketCode 0
    else do
      pkt <- liftIO $ getPacket
      case pkt of
        CallPacketCode -> do
          liftIO clearAbort
          processCallPacket
          liftIO endPacket
          liftIO newPacket
          answer
        _ -> do
          liftIO clearAbort
          return pkt

printString :: String -> IO ()
printString str = do 
  evaluate $ ExFunction "Print" [ ExString str ]
  return ()

processCallPacket :: ML ()
processCallPacket = do
  expr <- liftIO get
  case expr of
    ExInt n -> do
      config <- getConfig
      case n `IM.lookup` (functionTable config) of
        Just fn -> liftIO $ handle cleanUp (function fn)
        _ -> throwError $ MLErrorMsg "Function lookup failed."
    _ -> throwError $ MLErrorMsg "Expected int."
  where 
    cleanUp err = do 
      clearError
      printString $ "Error occurred in processing call: " ++ show (err :: SomeException)
      put $ ExSymbol "$Failed"
  
refuseToBeAFrontEnd :: IO ()
refuseToBeAFrontEnd = do
  putFunction "EvaluatePacket" 1
  putFunction "Module" 2
  putFunction "List" 1
  putFunction "Set" 2
  put meSym
  put plSym
  putFunction "CompoundExpression" 3
  putFunction "Set" 2
  put plSym
  getLink >>= (liftIO . (\l -> transferExpression l l))
  putFunction "Message" 2
  putFunction "MessageName" 2
  put plSym
  put $ ExString "notfe"
  put meSym
  put meSym
  endPacket
  waitForPacket (== SuspendPacketCode)
  where meSym = ExSymbol "me"
        plSym = ExSymbol "$ParentLink"

installFunctionTable :: ML ()
installFunctionTable = do
  liftIO activate
  functionPairs <- getConfig >>= (return . IM.toList . functionTable)
  mapM_ (liftIO . definePattern) functionPairs
  liftIO $ put $ ExSymbol "End"
  liftIO flush >>= maybeThrow

definePattern :: (Int,Function) -> IO ()
definePattern (ident,func) =
  put $ ExFunction "DefineExternal" [ ExString $ callPattern func
                                    , ExString $ argumentPattern func
                                    , ExInt ident
                                    ]

-- | Sends the given 'String' to /Mathematica/ for evaluation.
-- 
-- Does not block
evaluate :: Expression -> IO Bool
evaluate expr = do
  abrt <- checkAbort
  if abrt then return False else do
    put $ ExFunction "EvaluatePacket" [expr]
    endPacket
    return True

-- | Like 'evaluate', but blocks until the execution is complete.
evaluateAndWait :: Expression -> IO Bool
evaluateAndWait expr = handle 
  ((const $ return False) :: SomeException -> IO Bool) $
  do abrt <- checkAbort
     if abrt then return False else do
     result <- evaluate expr
     if result then
         waitForPacket (== ReturnPacketCode)
       else
         return ()
     return True

waitForPacket :: (PacketCode -> Bool) -> IO ()
waitForPacket q = do
  pkt <- getPacket
  newPacket
  if q pkt then return () else waitForPacket q

-- lifting utilities

maybeThrow :: Maybe MathLinkError -> ML ()
maybeThrow (Just err) = throwError $ MLException $ toException err
maybeThrow Nothing = return ()

valueOrThrow :: Either MathLinkError b -> ML b
valueOrThrow (Left err) = throwError $ MLException $ toException err
valueOrThrow (Right v) = return v

-- extra documentation

{- $usage
The following is a small Haskell module that exposes a function 
callable from /Mathematica/ that gets a pair of 'Int's (as a tuple) and 
returns their sum to /Mathematica/:

@
module Main where
 
import 'Foreign.MathLink'
 
addTwo :: 'IO' ()
addTwo = do
  (i1,i2) <- 'get'
  'put' ((i1 + i2) :: 'Int')

main = 'runMathLink' [ 'Function' \{ 'callPattern' = \"AddTwo[i_Integer,j_Integer]\"
                              , 'argumentPattern' = \"{i,j}\"
                              , 'function' = addTwo
                              \}
                   ]
@

A function to be exposed to /Mathematica/ has type @'IO' ()@. In its body 
it uses the 'get' function to receive a value from /Mathematica/, performs 
the desired computation, and sends the result back to /Mathematica/ via the 
'put' function.

The types that can be marshaled to\/from /Mathematica/ are instances of the
'Expressible' class.

See the @examples@ directory of the source distribution for more.
-}