Как стать автором
Обновить

Gambit Scheme: интегрируемся с Python

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров1.9K

Статья продолжает рассмотрение неочевидных возможностей 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-evalpython-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 (видео)

gambit/python (проект на гитхабе и его readme)

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+7
Комментарии0

Публикации

Работа

Data Scientist
42 вакансии

Ближайшие события