The Foreign Function Interface

Importing Foreign Code

In the import: section of a Sophie module, include a foreign import declaration. The following example is taken from the standard preamble:

import:

foreign "math" where

    e, inf, nan, pi, tau : number;

    acos, acosh, asin, asinh, atan, atanh,
    # ... bunch of more functions... #
    tan, tanh, trunc, ulp : (number) -> number;

    isfinite, isinf, isnan : (number) -> flag;

    atan2, comb, copysign, dist, fmod, ldexp, log_base@"log",
    nextafter, perm, pow, remainder : (number, number) -> number;

end;

A foreign import declaration contains:

  • The keyword foreign.

  • A literal string (like "math") that locates the foreign code. (Presently this means a Python module-path.)

  • Optionally, an initializer. More about that later.

  • And then usually:

    • The keyword where.

    • One or more foreign-import groups, ending in a semicolon.

    • The keyword end.

  • and finally, a semicolon.

A foreign-import group consists of:

  • A comma-separated list of names (but see note),

  • A colon,

  • A simple type annotation: named-types (including those defined in the same module) and function-types are fair game.

  • The semicolon at the end.

Note

Occasionally a foreign-symbol’s true name will differ from how you want to call it from Sophie. This example mentions log_base@"log", which means that the Python symbol is log but we want to use the name log_base in Sophie code for this instance of the import. In this case, the feature allows Sophie to expose the same underlying bit of Python with two different signatures. Python’s math.log takes an optional argument for the base of the logarithm (defaulting to e), but Sophie functions do not play that game.

Calling Conventions

Initializing a foreign (Python) module

The Basics

A foreign import-module can supply an initialization function called sophie_init. This is how you might write that:

import:
foreign "something.something.something" () where
    ...
    ...
end;

Note the empty pair of parenthesis after the foreign (Python) module-name. If these are present, the Sophie runtime will attempt to call an initialization function in the something.something.something module. The name of that function is always sophie_init. sophie_init will receive, as first argument, a forcing function which turns thunks into normal values but returns all other values as-is.

Exporting Sophie symbols to the foreign module

The foreign import-declaration can specify Sophie objects to pass in to said function, like so:

import:
foreign "sophie.adapters.turtle_adapter" (nil) ;

Note the parentheical (nil) here.

You can supply any comma-separated list of identifiers here. These can refer to any name that would be visible to the begin: section. In this case, the sophie.adapters.turtle_adapter module’s sophie_init function will receive, as second and subsequent arguments, a reference to the run-time representation of the corresponding symbols.

Note

It may be handy to pass in nil if your Python functions will do much with Sophie lists. nil is a variant-case which takes no arguments, and therefore it is guaranteed to be a singleton object. Using an is test on the Python side is probably slightly faster than inspecting nil’s subtype-tag.

Sophie calls a foreign function

Foreign functions are assumed to have strict (not lazy) semantics directly at the level of their parameters: They get passed evaluated values (not thunks) as arguments. This is fine for commandeering the pure functions from an existing ecosystem like Python. However, if the arguments are record-types, then the fields within likely contain thunks. If you need to force those thunks, recall that force argument to the initializer. Maybe stash a reference as a module-global variable.

Python calls back into Sophie code

On the Python side, a Sophie function appears as an object with an .apply(...) method. You can call that method with ordinary Python values as arguments, and the Sophie run-time will do the rest. What you get back may need force-ing. Perhaps it ought not. But that’s a deep subtlety I have not pondered sufficiently.

Caution

Although Sophie’s evaluator is re-entrant, nothing stops you from running out of stack space (recursion depth) on the Python side.

Providing Interaction (I/O Drivers)

The result of a Python module’s sophie_init function can specify linkages to I/O drivers.

For each expression in the begin: section, the run-time looks at the type of an object to decide how to interpret its contents. The sophie_init function in sophie.adapters.turtle_adapter binds a little something to the to drawing type:

def sophie_init(force, nil):
    ... # save nil for later # ...
    return {'drawing':do_turtle_graphics}

These driver-functions generally need to interact with the laziness inherent in the system. Continuing the turtle-graphics example, the driver’s prototype is:

def do_turtle_graphics(force, drawing):
    ...

The text of do_turtle_graphics can call force on a Sophie-object to get a strict-object. Now if that strict-object happens to be a record-like thing, then its fields may also be lazy / thunks, and so do_turtle_graphics is responsible to call force responsibly.

One last thing: I’ve passed Sophie’s nil into the turtle driver’s initializer because I know it will be a singleton object and I can thus use an is test in Python to detect the thing. That may make list-processing loops a hair faster.