DEV Community

Giovanni Crisalfi
Giovanni Crisalfi

Posted on • Edited on • Originally published at zwitterio.it

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")
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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")))
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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.

Originally posted on Zwitterionic Digressions

Top comments (0)