cabal update && cabal install json-python
json-python lets you call python from haskell by giving a high level interface for wrapping python functions. Hopefully this will let people interoperate between haskell and python with much less friction than using python's C API. The library uses json serialization for basic types and pointers for more complex python objects, making it great for either wrapping your exiting python projects with haskell or using a python library from a haskell script.
First, let's see an example-- Square.hs
import Python
square :: Int -> IO Int
square = defVV "export = lambda x: x * x"
main = do
x <- square 7
print x
$ runhaskell Square.hs
49
The defVV
function above has type (ToJSON a, FromJSON b) => String -> (a -> IO b)
. The type signature for square
further constrains a
to be of type Int
, and b
to be of type Int
as well. defVV
takes a string of python code defining a function with the name export
, and returns a high level interface to that function that can be called from haskell. We could have just as well written:
square :: Int -> IO Int
square = defVV "def export(x): return x * x"
or
square :: Int -> IO Int
square = defVV "from numpy import square as export"
What's imporant is that the python code defines a function export
of type Int -> IO Int
. If the python returns value that cannot be interpreted as an Int
, json-python will raise a haskell exception. Of course, we could have also defined a function of type String -> IO String
:
guess_type :: String -> IO String
guess_type = defVV "from mimetypes import guess_type; export = lambda x: guess_type(x)[0]"
or a function of type Float -> Float -> IO Float
:
sin_product :: Float -> Float -> IO Float
sin_product = defVVV "def export(x, y): from math import sin; return sin(x*y)
Where defVVV
is used to denote that the python function will take two arguments by Value and return a Value. But while defVV
and defVVV
let us easily wrap python functions, they come at a cost. Any variables that are passed to and from the python function must be serialized into a json string and then deserialized. At some level, this cost is inevitable. Using haskell data in python is going to requre instantiating python objects, which will require replicating memory. But if one is passing the same data into python again and again, it shouldn't be necessary to repeatedly serialize. Also, not every python data structure will implement json encoding and decoding. That's why json-python supports passing around direct references to python objects:
{-# LANGUAGE QuasiQuotes #-}
-- ShortestPaths.hs
import Python
data Graph
getGraph :: [(Int, Int)] -> IO (PyObject Graph)
getGraph = defVO [str|
import networkx
def export(xs):
g = networkx.Graph()
for edge in xs:
g.add_edge(edge[0], edge[1])
return g
|]
shortestPath :: (Int, Int) -> PyObject Graph -> IO Int
shortestPath = defVOV [str|
import networkx
def export(x, g):
return int(networkx.shortest_path_length(g, x[0], x[1]))
|]
main = do
g <- getGraph [(1,2),(2,3),(3,4)]
distance <- shortestPath (1,4) g
print distance
$ runhaskell ShortestPaths.hs
3
Notice the function defVO
above returns a graph object from the networkx library. It would be quite tricky to make sure that a json serialization of this object encoded all the necessary properties, but we don't have to. We just store a reference to that object. We may not be able to manipulate the object directly in haskell, but we can pass it to other functions wrapped with json-python, such as the shortestPath
function defined above. Even better, the object passing in json-python is implemented with Foreign Pointers, so when g
has no remaining references, it will decrement the python reference count to the object, causing it to be garbage collected in python. Even though json-python manipulates python with the C API, you don't have to do any manual memory management. In fact, json-python should never cause a SegFault (if it does, please report it as a bug).
By now, you may have noticed a pattern in the naming convention of the def
functions. There is one letter for each of the arguments and for the return type. A V
indicates that the type will be passed as a json value. A O
indicates that the type will be passed as a pointer to a python object. You can use any combination of O
and V
describing python functions with up to three arguments. So defV
, defVOOV
, and defOOVV
are all available to you after you write import Python
.
json-python is still very much in the alpha stage. The technology for catching python exceptions and bubbling them up into haskell exceptions isn't finalized, and it hasn't been tested for thread safety with concurrent IO. Nonetheless, I'm really excited to see what people can do with it.