package ocaml-in-python
Install
Dune Dependency
Authors
Maintainers
Sources
sha512=9ba2ad109ce83a758dd949fc40be8e866adb5aebf3b2009a04c4d93ea40f48ca71b8d6f8cd4e80a2bf52ca36fab6561f28e273d412cf8c235837063924f26eff
Description
Effortless Python bindings for OCaml modules
Published: 28 Mar 2022
README
Effortless Python bindings for OCaml modules
This library exposes all OCaml modules as Python modules, generating bindings on the fly.
Requirements
OCaml
>= 4.13Python
>= 3.7 (and >= 3.10 for pattern-matching support)
Setup
The package can be installed via opam
:
opam install ocaml-in-python
installs the latest release,opam pin add -k path . && opam install ocaml-in-python
executed in a clone of this repository installs the latest development version.
Once installed via opam
, the package should be registered in the Python environment. There are two options:
either you register the package with
pip
using the following command:
pip install --editable "`opam var ocaml-in-python:lib`"
or you add the following definition to your environment:
export PYTHONPATH="`opam var share`/python/:$PYTHONPATH"
Examples
Standard library
A very simple mean to test that the bindings are working properly is to invoke the OCaml standard library from Python.
import ocaml
print(ocaml.List.map((lambda x : x + 1), [1, 2, 3]))
# => output: [2;3;4]
In the following example, we invoke the ref
function from the OCaml standard library to create a value of type int ref
(a mutable reference to an integer), and the following commands show that the reference can be mutated from Python (a reference is a record with a mutable field contents
) and from OCaml (here by invoking the OCaml function incr
).
>>> x = ocaml.ref(1, type=int)
>>> x
{'contents':1}
>>> x.contents = 2
>>> x
{'contents':2}
>>> ocaml.incr(x)
>>> x
{'contents':3}
OCaml module compiled on the fly
In the following example, we compile a module on the fly from Python.
import ocaml
m = ocaml.compile(r'''
let hello x = Printf.printf "Hello, %s!\n%!" x
type 'a tree = Node of { label : 'a; children : 'a tree list }
let rec height (Node { label = _; children }) =
1 + List.fold_left (fun accu tree -> max accu (height tree)) 0 children
let rec of_list nodes =
match nodes with
| [] -> invalid_arg "of_list"
| [last] -> Node { label = last; children = [] }
| hd :: tl -> Node { label = hd; children = [of_list tl] }
''')
m.hello("world")
# => output: Hello, world!
print(m.height(
m.Node(label=1, children=[m.Node(label=2, children=[])])))
# => output: 2
print(m.of_list(["a", "b", "c"]))
# => output: Node {label="a";children=[Node {label="b";children=[Node {label="c";children=[]}]}]}
try:
print(m.of_list([]))
except ocaml.Invalid_argument as e:
print(e)
# => output: Stdlib.Invalid_argument("of_list")
It is worth noticing that there is no need for type annotations: bindings are generated with respect to the interface obtained by type inference.
Requiring a library with findlib
In the following example, we call the OCaml library parmap
from Python.
import ocaml
ocaml.require("parmap")
from ocaml import Parmap
print(Parmap.parmap(
(lambda x : x + 1), Parmap.A([1, 2, 3]), ncores=2))
# => output: [2, 3, 4]
The function ocaml.require
uses ocamlfind
to load parmap
. Bindings are generated as soon as ocaml.Parmap
is accessed (in the example, at line from ocaml import Parmap
). Parmap.A
is one of the two constructors of the type Parmap.sequence
.
Conversion rules
The generation of bindings is driven by the types exposed by the compiled module interfaces (*.cmi
files): relying on the *.cmi
files allows the bindings to cover most of the OCaml definitions (there are some limitations though, see below) and to use the inferred types for modules whose interface is not explicitly specified by a .mli
file.
Built-in types
The following conversions are defined for built-in types:
OCaml
int
,nativeint
int32
,int64
are mapped to Pythonint
;
import ocaml
ocaml.print_endline(ocaml.string_of_int(42))
# => output: 42
print(ocaml.int_of_string("5") + 1)
# => output: 6
OCaml
string
is mapped to Pythonstr
import ocaml
ocaml.print_endline("Hello, World!")
# => output: Hello, World!
print(ocaml.String.make(3, "a") + "b")
# => output: aaab
OCaml
char
is mapped to Pythonstr
with a single character
import ocaml
print(ocaml.int_of_char("a"))
# => output: 97
print(ocaml.char_of_int(65))
# => output: A
OCaml
bool
is mapped to Pythonbool
(beware of different case convention: OCaml valuesfalse
andtrue
are mapped to Python valuesFalse
andTrue
respectively)
import ocaml
print(ocaml.Sys.interactive.contents)
# => output: False
print(ocaml.string_of_bool(True))
# => output: true
OCaml
float
is mapped to Pythonfloat
, and functions taking floats as arguments can take benefit from the Python automatic coercion fromint
tofloat
import ocaml
print(ocaml.float_of_int(1))
# => output: 1.0
print(ocaml.cos(0))
# => output: 1.0
OCaml
array
is mapped to a dedicated classocaml.array
, which supports indexing, enumeration, pattern-matching (with Python >= 3.10) and in-place modification. When an OCaml array is converted to a Python object, the elements are converted on demand. There is an implicit coercion to array from all Python iterable types such as Python lists (but in-place modification is lost).
import ocaml
arr = ocaml.Array.make(3, 0)
arr[1] = 1
print(ocaml.Array.fold_left((lambda x,y : x + y), 0, arr))
# => output: 1
ocaml.Array.sort(ocaml.compare, arr)
print(list(arr))
# => output: [0, 0, 1]
print(ocaml.Array.map((lambda x: x + 1), range(0, 4)))
# => output: [|1;2;3;4|]
# With Python 3.10:
match arr:
case [0, 0, 1]:
print("Here")
# => output: Here
It is worth noticing that Array.make
is a polymorphic function parameterized in the type of the elements of the constructed array, and by default the type parameter for polymorphic function with ocaml-in-python
is Py.Object.t
, the type of all Python objects. As such, the cells of the array arr
defined above can contain any Python objects, not only integers.
arr[0] = "Test"
print(arr)
# => output: [|"Test";0;1|]
We can create an array with a specific types for cells by expliciting the type parameter of Array.make
, by using the keyword parameter type
.
arr = ocaml.Array.make(3, 0, type=int)
arr[0] = "Test"
# TypeError: 'str' object cannot be interpreted as an integer
OCaml
list
is mapped to a dedicated classocaml.list
, which supports indexing, enumeration and pattern-matching (with Python >= 3.10). When an OCaml list is converted to a Python object, the elements are converted on demand. There is an implicit coercion to list from all Python iterable types such as Python lists.OCaml
bytes
is mapped to a dedicated classocaml.bytes
, which behaves as a mutable collection of characters.OCaml
option
is mapped to a dedicated classocaml.option
, only for values of the formSome x
where the type ofx
allows the valueNone
. If the type ofx
does not contain a valueNone
, the OCaml valueSome x
is mapped directly to the conversion ofx
. Conversely, the valueSome x
can be constructed withocaml.Some(x)
. The OCaml valueNone
is mapped to the Python valueNone
.
print(ocaml.List.find_opt((lambda x : x > 1), [0,1], type=int))
# => output: None
print(ocaml.List.find_opt((lambda x : x > 1), [0,1,2], type=int))
# => output: 2
print(ocaml.List.find_opt((lambda x : x > 1), [0,1,2]))
# => output: Some(2)
In the last call to find_opt
, the default type parameter is Py.Object.t
which contains the value None
.
OCaml
exn
is mapped to a dedicated classocaml.exn
, which is a sub-class of PythonError
class, and exceptions are converted as other extension constructors: each exception is a sub-class ofocaml.exn
, and values can be indexed (if the exception constructor takes parameters), accessed by field name (for inline records) and supports pattern-matching (with Python >= 3.10). There is an implicit coercion from other sub-classes of PythonError
class toPy.E
, the OCaml exception defined inpyml
for Python exceptions. If an exception is raised between OCaml and Python code, the exception is converted and raised from one side to the other.
try:
ocaml.failwith("Test")
except ocaml.Failure as e:
print(e[0])
# => output: Test
OCaml
in_channel
andout_channel
are mapped toFileIO
objects. In the following example, the OCaml functionopen_out
is used to create a new filetest
, and the stringHello
is written in this file through the Python methodwrite
. Then, the filetest
is opened for reading with the Python built-inopen
, and the channel is read with the OCaml functionreally_input_string
.
with ocaml.open_out("test") as f:
f.write(b"Hello")
with open("test", "r") as f:
print(ocaml.really_input_string(f, 5))
# => ouput: Hello
OCaml
floatarray
is mapped to a dedicated classocamlarray
, which derives fromnumpy
array (numpy
is required for the support offloatarray
).
Type constructors
OCaml functions of type
't_1 -> ... -> 't_n -> 'r
are mapped to Python callable objects withn
arguments. Labelled arguments are mapped to mandatory keyword arguments, and optional arguments are mapped to optional keyword arguments. For polymorphic functions, the type parameters are assumed to bePy.Object.t
, except if there is a keyword argumenttype
: the associated value can either be a single type if there is a single type parameter, or a tuple of types giving the type parameters in the order of their apparition in the function signature, or a dictionary whose keys are the names of the type parameters (e.g.,"a"
for'a
).OCaml tuples of type
't_1 * ... * 't_n
are mapped to OCaml tuples withn
components.
Type definitions
Each OCaml type definition introduces a new Python class, except for type aliases, that are exposed as other names for the same class.
Records are accessible by field name or index (in the order of the field declarations), and the values of the fields are converted on demand. Mutable fields can be set in Python. In particular, the ref
type defined in the OCaml standard library is mapped to the Python class ocaml.ref
with a mutable field content
. Records support pattern-matching (with Python >= 3.10). There is an implicit coercion from Python dictionaries with matching field names.
For variants, there is a sub-class by constructor, which behaves either as a tuple or as a record. The values of the arguments are converted on demand. Variants support pattern-matching (with Python >= 3.10).
Sub-module definitions
Sub-modules are mapped to classes, which are constructed on demand. For instance, the module Array.Floatarray
is exposed as ocaml.Array.Floatarray
, and, in particular, the function Array.Floatarray.create
is available as ocaml.Array.Floatarray.create
.
Limitations
The following traits of the OCaml type system are not supported (yet):
records with polymorphic fields,
polymorphic variants,
objects,
functors,
first class modules.