Getting Started with Hy, the Python Lisp: a Matplotlib example (ep. 2)
In the previous episode, we briefly saw what Hy is, what it means that it is a dialect of Lisp, how to install it, and step by step, we wrote a small example to create a line plot in matplotlib, strictly following the imperative approach we would have used in Python.
(import matplotlib.pyplot :as plt)
(setv x-values [1 2 3 4 5])
(setv y-values [2 4 6 8 10])
(setv [fig ax] (plt.subplots))
(ax.plot x-values y-values :marker "o")
(ax.set-xlabel "X values")
(ax.set-ylabel "Y values")
(ax.set-title "Simple Line Plot")
(plt.savefig "img/hy-plt-example.png")
If you don't understand something about the code above, I recommend referring to the first part: maybe something just went unnoticed until now. If, instead, everything is clear, we can proceed. Now, it's time to make things more spicy by adopting a functional approach.
More precisely, we will begin by encapsulating everything in a let
.
What is a let?§
Before rewriting our code, it is important to understand what a "let" is and why it is so useful. If you are already familiar with this concept, feel free to skip this paragraph and proceed directly to the code.
A "let" is a Lisp operator that allows you to create a lexical context with new local variables. Basically, it's like saying you want to call a function that can access these variables and prioritize these variable declarations over other external ones with the same name.
How does it works?
A let expression has two parts. First comes a list of instructions for creating variables, each of the form (variable expression). Each variable will initially be set to the value of the corresponding expression. [...] After the list of variables and values comes a body of expressions, which are evaluated in order. (Graham, ANSI Common Lisp, 1996, p. 20)
In Hy terms,
(let [x 2]
(print x))
The x
variable defined in the head of the let is not accessible outside it.
Actually, the Hy let is pretty special, because it makes you write variables that have access to the ones defined previously in the same expression. For example,
(let [x 2
y 3
z (+ x y)]
(print x)
(print y)
(print z))
As you can see here, the z
variable is defined in relation to x
and y
.
As the documentation explains,
Like the let* of many other Lisps, let executes the variable assignments one-by-one, in the order written
This means that, as let*
in other lisps, let
in Hy is functionally equivalent to a series of pure nested lets.
Let('s) plot§
Being conscious of how let
works in Hy, we can now rewrite the initial code:
(import matplotlib.pyplot :as plt)
(let [x-values [1 2 3 4 5]
y-values [2 4 6 8 10]
[fig ax] (plt.subplots)]
(ax.plot x-values y-values :marker "o")
(ax.set-xlabel "X values")
(ax.set-ylabel "Y values")
(ax.set-title "Simple Line Plot")
(plt.savefig "img/hy-plt-example.png"))
Remember that let blocks can be nested. For example, in the case we want to plot two identical graphs but with different labels, we could write:
(import matplotlib.pyplot :as plt)
(let [x-values [1 2 3 4 5]
y-values [2 4 6 8 10]]
; English version
(let [[fig ax] (plt.subplots)]
(ax.plot x-values y-values :marker "x")
(ax.set-xlabel "X values")
(ax.set-ylabel "Y values")
(ax.set-title "First Title")
(plt.savefig "img/hy-plt-example-en.png"))
; Italian version
(let [[fig ax] (plt.subplots)]
(ax.plot x-values y-values :marker "o")
(ax.set-xlabel "Ascisse")
(ax.set-ylabel "Ordinate")
(ax.set-title "Secondo titolo")
(plt.savefig "img/hy-plt-example-it.png")))
This code saves two figures: an English one, and an Italian one.
Macros§
Lisp code, including Hy code, is expressed as a series of lists. Its minimalist syntax (or rather, the lack of it) allows for easy writing of macros. But what is a macro?
The most common way to write programs that write programs is by defining macros. Macros are operators that are implemented by transformation. You define a macro by saying how a call to it should be translated. This translation, called macro-expansion, is done automatically by the compiler. So the code generated by your macros becomes an integral part of your program, just as if you had typed it in yourself. (Graham, ANSI Common Lisp, 1996, p. 162)
In other words, macros are operators that write code and lisps are particularly good for metaprogramming, which means the code itself is treated as data that can be manipulated by itself, allowing for powerful generative techniques. This means also that we can build up our own language to make the code drier or more comfortable.
Lisp is designed to be extensible: it lets you define new operators yourself. (Graham, ANSI Common Lisp, 1996, p. 3)
Going meta§
Let's see how we can leverage the power of macros in this case. If you require a series of similar plots with only the title and filename being changed, you can write:
(import matplotlib.pyplot :as plt)
(defmacro plot-with-title [title fname]
(quasiquote (let [[fig ax] (plt.subplots)]
(ax.plot x-values y-values :marker "o")
(ax.set-xlabel "Ascisse")
(ax.set-ylabel "Ordinate")
(ax.set-title (unquote title))
(plt.savefig (unquote fname)))))
(let [x-values [1 2 3 4 5]
y-values [2 4 6 8 10]]
(plot-with-title "First title" "img/hy-plt-example-one.png")
(plot-with-title "Second title" "img/hy-plt-example-two.png"))
We can dream bigger by creating abstractions for all line plots.
(import matplotlib.pyplot :as plt)
(defmacro lineplot [x y title fname marker xlabel ylabel]
(quasiquote (let [x-values (unquote x)
y-values (unquote y)
[fig ax] (plt.subplots)]
(ax.plot x-values y-values :marker (unquote marker))
(ax.set-xlabel (unquote xlabel))
(ax.set-ylabel (unquote ylabel))
(ax.set-title (unquote title))
(plt.savefig (unquote fname)))))
(lineplot [1 2 3 4 5]
[2 4 6 8 10]
"Titolo"
"img/hy-plt-example-one.png"
"o"
"X values"
"Y values")
As long as the macro definition is under your eyes, it's easy to understand the order in which to write the arguments. In this example: first the values for x and y, then the title, followed by the path of the image to be saved, the marker, and the labels. But this is just a simplified situation: usually, the macro definition is hidden in a library out of sight, so named arguments should be preferred over positional arguments.
Unfortunately, this is not directly possible in Hy because, as described in the documentation,
defmacro
cannot use keyword arguments, because all values are passed to macros unevaluated. All arguments are passed positionally, but they can have default values:
(defmacro a-macro [a [b 1]]
`[~a ~b])
;; (a-macro 2)
;; [2 1]
;; (a-macro 2 3)
;; [2 3]
;; (a-macro :b 3)
;; [:b 3]
Thankfully, there's a nifty workaround to achieve to have both the default values and the positional arguments. Instead of them being assigned in the macro, we write a simple wrapper function. Then we use the function for:
- Defining the default values;
- Giving names to macro's positional arguments.
(import matplotlib.pyplot :as plt)
(defmacro macro-lineplot [x
y
title
fname
marker
xlabel
ylabel]
(quasiquote (let [x-values (unquote x)
y-values (unquote y)
[fig ax] (plt.subplots)]
(ax.plot x-values y-values :marker (unquote marker))
(ax.set-xlabel (unquote xlabel))
(ax.set-ylabel (unquote ylabel))
(ax.set-title (unquote title))
(plt.savefig (unquote fname)))))
(defn lineplot [x
y *
[title "Title"]
[fname "fname.png"]
[marker "o"]
[xlabel "X values"]
[ylabel "Y values"]]
(macro-lineplot x y title fname marker xlabel ylabel))
The asterisk (*) tells which parameters need the key:
If the symbol * is given in place of a parameter, it means that all the following parameters can only be set by name.
In other words, we have assigned default values that can be overridden by using the key's name.
Ultimately, assuming the macro is externalized in a library, the original code simplifies to these practical and fully functional lines:
(lineplot [1 2 3 4 5]
[2 4 6 8 10]
:title "Simple Line Plot"
:fname "img/hy-plt-example-last.png")
Of course, this is the simplest example I could think of, and there is room for a lot of improvement, but I think it makes the potential of the language and macros quite evident.
Moreover, keep in mind that the thrill lies in the fusion of these two powerful languages. By keeping the hy
package as a dependency, Hy macros can be imported and used directly in Python code. This allows you to easily share the wrapper function with non-Hy users.
Conclusion§
In this post, we have discussed how to effectively use lexical scoped blocks and macros. Macros provide a powerful tool for metaprogramming, allowing you to extend the language and write code that is both cleaner and more expressive. By understanding and utilizing these features in Hy, you will be able to write more reliable code while still benefiting from Python libraries.
Did you find this post useful?
Remember that this website doesn’t make use of any trackers or analytics or adv so it doesn’t earn from your visits (moreover, it has a minimal environmental impact).
If you like this blog, refill my caffeine supplies by