call-with.cc

Hy Times in Python, blending Hy and PySide6

Leveraging meta-programming to create interfaces using PySide6

Hi, let's meet Hy

Hy is dialect of Lisp that is embedded right into Python. This simple syntax brings with it the world of meta-programming, that is, the ability to craft new code objects at compile time based on functions called macros. In this post we'll look at a macro that in turn helps create a nice DSL (domain specific language) for creating Qt QWidgets. For those unfamiliar with Qt, it is a powerful cross platform C++ UI framework for creating application interfaces, and PySide is their official bindings for Python.

Working with PySide6

Let's that a look at this example found in the PySide documentation to see what using Python to build an interface looks like:

import sys
import random

from PySide6.QtWidgets import (QApplication, QLabel,
     QPushButton, QVBoxLayout, QWidget)
from PySide6.QtCore import Qt, Slot

class MyWidget(QWidget):
    def __init__(self):
        super().__init__()

        self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир"]

        self.button = QPushButton("Click me!")
        self.text = QLabel("Hello World")
        self.text.setAlignment(Qt.AlignCenter)

        self.layout = QVBoxLayout()
        self.layout.addWidget(self.text)
        self.layout.addWidget(self.button)
        self.setLayout(self.layout)

        self.button.clicked.connect(self.magic)

    @Slot()
    def magic(self):
        self.text.setText(random.choice(self.hello))

Well, for starters, one thing not to like is Python's syntax itself... (I kid... I kid... but not really 😅) But in all seriousness, it is such a drag to me have to write that ridiculous __init__ key, and then remember to call it, off of a call to super, just to get started creating QWidgets. Secondly, there is this matter of the @Slot decorator, needed to create the QWidget slots.

PyQt... I mean, HyQt

Not the worst API, but lets compare it to this:

(defQWidget MyWidget
  (fn [self]
       (setv self.hello '("Hello Welt" "Hei maailma" "Hola Mundo" "Привет мир"))

       (setv self.button (QPushButton "Click Me!"))
       (setv self.text (QLabel "Hello World"))
       (self.text.setAlignment Qt.AlignCenter)

       (setv self.layout (QVBoxLayout))
       (self.layout.addWidget self.text)
       (self.layout.addWidget self.button)
       (self.setLayout self.layout)

       (self.button.clicked.connect self.magic))
  (defn magic [self]
     (self.text.setText (random.choice self.hello))))

The API chooses to create a new syntax item, defQWidget explicitly for the purpose of creating QWidgets. It accepts a first argument, the name of the class for the widget, and then accepts an anonymous function that receives self. This first required function is the components initialization. It then accepts optional, and potentially many, named functions. The names of these functions become the members/slots of the QWidget.

It's not a huge change, and it may prove too inflexible as more complex examples are thrown at it, but I'm certain and comfortable that the macro has room to expand in complexity while still proving to be an improvement to the original syntax.

Resulting GUI

The macro itself is created using defmacro, let's take a look at it here:

(defmacro defQWidget [name init #* slots]
          "Macro to facilitate the creation of QWidget classes."
          `(defclass ~name [QWidget]
             (defn __init__ [self]
               (. (super) (__init__))
               (~init self)
               None)
             ~@(lfor slot slots
                     `(with-decorator (Slot)
                                      ~slot))))

Hy makes creating macros a very straight forward process, allowing a lovely DSL to emerge while simultaneously obscuring some of the tedious boiler plate around UI creation in Python. It is worth pointing out that it is not a Lisp or a Scheme, it is a Lispy Python that sits on top of Python data types and focuses on ease of interoperability. It's an interesting thought that not being a Lisp or Scheme cuts it off from a world of libraries and SRFI's in those languages, but ironically its tradeoff in supporting Python exposes it to a much more robust universe of libraries beyond any lisp.