Статья продолжает рассмотрение неочевидных возможностей Gambit Scheme, начатое в предыдущих статьях.
На этот раз расскажем о том, как использовать в программах на Gambit Scheme код на языке Python, в том числе многочисленные библиотеки, разработанные для Python.
Постановка проблемы
По некоторым подсчётам, язык Python в настоящее время является наиболее популярным среди языков программирования. Не так важно, верно это или неверно в точности, но, во всяком случае, популярность Питона очень высока. Вместе с довольно хорошо продуманной питоновской модульной системой это привело к тому, что для этого языка разработано огромное количество практически полезных и часто хорошо отлаженных библиотек, количество которых в одном только стандартном репозитории PyPl оценивается более чем в 600 000. Это число намного превосходит те несколько тысяч библиотек, которые общедоступны для языка Scheme. В том числе, для Python доступны основные библиотеки нейросетевого искусственного интеллекта, что популярно и молодёжно, а в ряде случаев и приносит практическую пользу.
С другой стороны, язык Scheme обладает отточенностью и практически неограниченной расширяемостью синтаксиса и семантики, позволяющими добиться значительной плотности смыслового содержания кода, а во многих случаях и формально доказывать правильность кода. Также язык Scheme очень хорошо себя показывает в приложениях символического искусственного интеллекта, в том числе продукционного вывода, автоматической генерации текстов программ и построения предметно-ориентированных языков. Свойство гомоиконичности, характерное для всех производных Лиспа, оказывается незаменимым при написании самомодифицирующегося кода.
Возникает естественное желание совместить достоинства Scheme и Python в своих проектах. Эта задача решена в нескольких системах программирования, мы здесь рассмотрим её применение в Gambit Scheme, интегрируемой с CPython.
Инсталляция
Интеграция Gambit Scheme и Python недоступна в формате "целиком прямо из коробки", но может быть достигнута небольшими усилиями. К сожалению, соответствующие средства достаточно слабо и фрагментарно документированы, что мы и постараемся восполнить настоящей статьёй.
Интеграция возможна в настоящий момент под управлением операционных систем Linux, macOS и Windows. Мы для определённости рассмотрим необходимые действия в Debian 12. Для других дистрибутивов Linux и для macOS последовательность действий в целом аналогична, для Windows потребуются небольшие уточнения, за которыми мы отошлём читателя к приведённым в конце статье источникам.
Общим ограничением является использование транслятора Python (CPython) версии не ниже 3.7.
Итак, рассмотрим всю последовательность необходимых шагов с нуля.
Устанавливаем Debian 12.
Устанавливаем пакеты, которые нам понадобятся для сборки Gambit Scheme и интерфейса Scheme с Python:
sudo apt-get install gcc make python3-dev python3-pip python3.11-venv
Тут же можно установить и python3-tk, который мы будем использовать в примере.
Скачиваем исходные тексты Gambit Scheme с его сайта. Готовый пакет из дистрибутива ОС нам тут не подойдёт, так как придётся перекомпилировать в нестандартном режиме (кроме того, сборка Gambit Scheme в дистрибутиве Debian в любом случае имеет проблемы с отображением символов Unicode).
Распаковываем:
tar xvf gambit-v4_9_5.tgz
cd gambit-v4_9_5
Теперь производим конфигурирование и сборку с ключом
--enable-multiple-threaded-vms
, не используемым по умолчанию, но необходимым для взаимодействия с CPython:
./configure --enable-multiple-threaded-vms --enable-single-host --enable-march=native
make
sudo make install
sudo ln /usr/local/Gambit/bin/* /usr/local/bin/
Теперь у нас есть команды для вызова интерпретатора и компилятора
gsi
иgsc
, что можно проверить, выдав их без параметров. Устанавливаем интерфейс к CPython:
gsi -install github.com/gambit/python
gsc github.com/gambit/python
Если мы используем Linux, то перед загрузкой Gambit Scheme необходимо руками подгружать динамическую библиотеку CPython. Это можно сделать, например, добавив в
.bash_profile
строчки (видоизменяемые в зависимости от конкретной версии Питона):
alias gsc='LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libpython3.11.so gsc'
alias gsi='LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libpython3.11.so gsi'
Всё! Если не возникло непредвиденных ошибок, всё должно работать.
Теперь, наконец, разберёмся, что же собственно и как должно работать.
Описание системы программирования
Gambit Scheme поддерживает практически свободное перемешивание кода Scheme и Python в программе (возьмём на себя смелость назвать такой код билингвальной программой, или просто билингвой). Для этого в операционной семантике билингвы используется вызов CPython через интерфейс динамической библиотеки, а в денотационной – несколько системных функций низкого уровня и специальный синтаксис SIX (Scheme Infix eXtension) Python.
Билингва может свободно использовать библиотеки для языка Python. Импортировать из PyPl в виртуальное окружение Python, используемое билингвой, проще всего функцией-обёрткой над PIP, pip-install
. Чтобы работал следующий пример, выполним в gsi команды:
(import (github.com/gambit/python))
(pip-install "matplotlib")
(pip-install "networkx")
Можно было бы того же самого достичь через вызов pip3
из виртуального окружения:
.gambit_userlib/.venv3.11/bin/pip3 install matplotlib networkx
Сама билингва представляет собой обычный исходный файл .scm или .sld, содержащий в преамбуле импорт синтаксиса SIX Python и интерфейса к CPython:
(import (_six python) (github.com/gambit/python))
Это же, в конце концов, Лисп, поэтому мы можем вот так вот просто импортировать новый синтаксис (и семантику). После этого SIX позволяет нам перемешивать инфиксные операторы, написанные в синтаксисе Python, с обычными префиксными операторами Scheme.
Рассмотрим для примера конкретную билингву для Linux (не пытайтесь буквально повторить это в macOS из-за особенностей matplotlib):
(import (_six python) (github.com/gambit/python))
\import warnings
\warnings.filterwarnings("ignore", category=UserWarning)
;; фильтр, чтобы matplotlib не ругался
;; на то, что он не в главной нитке
\import matplotlib
\matplotlib.use("TkAgg")
\import networkx as nx
\G=nx.Graph()
(define nodes '(a b c d e))
(define edges '((a b) (b c) (c d) (d e) (a d)))
\G.add_nodes_from(`nodes)
\G.add_edges_from(`edges)
\nx.draw(G, with_labels=True, font_weight="bold")
\matplotlib.pyplot.show()
Мы видим, что лексемы в инфиксном синтаксисе выделяются обратной косой чертой. Тут есть нюанс – черта сама по себе действует либо до окончания синтаксической конструкции (например, оператора import
или внутренности каких-либо скобок), либо до пробельного символа. Поэтому в билингве, в отличие от Питона, имеет значение, что символ =
в присваивании или левая скобка в вызове метода не отделяется пробелом.
Переменные Scheme и Python имеют разные лексические области видимости и разные типы. Типы переменных Python имеют внутреннее представление CPython и наследуются от PyObject, как им и положено. Поэтому, если мы напишем:
(define v 1)
то имеется в виду скимовская переменная v, которая получит значение 1
скимовского типа fixnum
, а если мы напишем:
\v=2
то имеется в виду питоновская переменная v, которая получит значение 2
питоновского типа int
:
> (pp v)
1
> \print(v)
2
Однако, питоновский код может использовать скимовские переменные с соответствующим преобразованием типа через символ квазицитирования `
:
> \print(`v)
1
Точно также и скимовский код может использовать питоновские переменные с преобразованием типа через черту:
> (pp \v)
2
Также можно взаимно вызывать и функции (которые и в Scheme, и в Python являются объектами первого класса).
Кроме того, вычислить значение строки, представляющей собой код на Питоне, в самом натуральном CPython можно при помощи функции python-eval
(и python-exec
, которая делает то же, не возвращая значения):
> (python-eval "print('Hello')")
Hello
Аналогично же и питоновский код при необходимости может вычислять скимовские выражения через обычный скимовский eval
:
> \scheme_eval=`(lambda (s) (eval (call-with-input-string s read)))
> \scheme_eval("(pp 'Hello)")
Hello
Таблица соответствия типов Scheme и Python представлена ниже:

Теперь, запустив нашу билингву, мы уже можем понять, что в ней происходит. В скиме создаются списки, представляющие собой множества вершин и рёбер графа, а питоновские библиотеки matplotlib и networkx их отображают на графике:

Конечно, в данном конкретном случае было бы ещё элегантнее весь питоновский код вынести в отдельный модуль .py (или .six), а в .scm ограничиться импортом и вызовом этого модуля.
Можно, впрочем, просто использовать многострочные строковые литералы Scheme:
(import python) ;; six здесь не используется
(py-exec #<<end
class Foo;
def __init__ (self, f):
self.f = f
foo = Foo(10)
print (foo)
end
)
В качестве другого примера перемножим матрицы при помощи NumPy:
(import (_six python) (github.com/gambit/python))
\import numpy as np
(define a '((5 6) (7 8) (1 2) (3 4)))
(define b '((1 2 3 4) (5 6 7 8)))
\result=np.array(`a)@np.array(`b)
\print(result)
(pp \result.tolist())
[[35 46 57 68]
[47 62 77 92]
[11 14 17 20]
[23 30 37 44]]
((35 46 57 68) (47 62 77 92) (11 14 17 20) (23 30 37 44))
Честно говоря, эта программа, несмотря на использование билингвы, выглядит гораздо проще, чем изучение библиотеки SRFI 231, представляющей собой реализацию массивов общего вида в Scheme, да и вдобавок к NumPy, в сердцевине которой под обёрткой из Си сидит старый добрый Фортран, больше доверия в смысле численных нюансов. Здесь мы фактически в нескольких строках скомплексировали бутерброд из четырёх языков, каждый из которых занимает подходящее ему место: Scheme – языка для написания алгоритма верхнего уровня, Python – клея для библиотек, C – системного интерфейса, Fortran – средства для численных расчётов.
Следует обратить внимание вот ещё на какой факт. Если бы мы в нашей последней билингве написали просто:
(pp \result)
то получили бы что-то вроде:
#<PyObject* #2 0x1085758f0>
Это происходит потому, что скимовская функция pp
видит значение питоновской переменной result
благодаря черте, но не умеет печатать питоновские объекты, а автоматического преобразования к типу Scheme не происходит, так как массив NumPy не входит в приведённую выше таблицу преобразуемых типов. Поэтому нам пришлось в оригинальном коде руками вызвать метод .tolist()
для преобразования в известный Scheme тип. Хотя при желании мы можем модифицировать среду Scheme для печати любых питоновских объектов в человекочитаемом виде, аналогично тому, как мы это делали с массивами байтов в предыдущей статье (то есть заставив системный форматировщик Scheme применять питоновскую функцию \str()
к объектам типа PyObject).
Заключение
Более глубоко о способе реализации связи между Gambit Scheme и CPython можно узнать из источников, перечисленных ниже.
Источники
A Foreign Function Interface between Gambit Scheme and CPython (статья)
Scheme 2021 - A lightweight approach for accessing Python modules from Gambit Scheme (видео)