thierry-martinez / ocaml-in-python

Effortless Python bindings for OCaml modules
BSD 2-Clause "Simplified" License
50 stars 2 forks source link

Effortless Python bindings for OCaml modules

This library exposes all OCaml modules as Python modules, generating bindings on the fly.

Requirements

Setup

The package can be installed via opam:

Once installed via opam, the package should be registered in the Python environment. There are two options:

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 an OCaml 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:

import ocaml

ocaml.print_endline(ocaml.string_of_int(42))
# => output: 42
print(ocaml.int_of_string("5") + 1)
# => output: 6
import ocaml

ocaml.print_endline("Hello, World!")
# => output: Hello, World!
print(ocaml.String.make(3, "a") + "b")
# => output: aaab
import ocaml

print(ocaml.int_of_char("a"))
# => output: 97
print(ocaml.char_of_int(65))
# => output: A
import ocaml

print(ocaml.Sys.interactive.contents)
# => output: False
print(ocaml.string_of_bool(True))
# => output: true
import ocaml

print(ocaml.float_of_int(1))
# => output: 1.0
print(ocaml.cos(0))
# => output: 1.0
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
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.

try:
    ocaml.failwith("Test")
except ocaml.Failure as e:
    print(e[0])
# => output: Test
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

Type constructors

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):