Hy is an exciting new language that has surfaced just recently. Essentially, it is what Clojure is for Java-a Lisp implementation that has a very tight relationship with the host language, which is Python in this case. Hy allows developers to write their programs using its strong functional capabilities while leveraging the vast libraries of Python.
In this article, let’s write a simple phone register program that stores people and their phone number in a Sqlite database. The program will have a textual user interface. The source code is available at GitHub: https://github.com/tuturto/register.
Pre-requisities
There are only a very few software components that we need. The first is Python. Version 2.7 or newer should work. The next important component is the Hy language itself. It can be installed with Pip:
sudo pip install Hy
The third component is the Sqlite database. Installation instructions depend on the package manager you are using. With Apt for example, the command is as follows:
sudo apt-get install sqlite3
A gentle introduction
Hy is a Lisp variant, which means that the programs might look quite different compared to those written with other languages-the biggest difference being that everything looks like a list and the operation is the first element in the list. For example, to calculate 1 + 1, you need to type the following code:
(+ 1 1)
And the ever familiar ‘hello world’ would be written as:
(print "Hello World!")
To define a function that compares two numbers, you could use the following commands:
(defn comparison [x y] (cond ((= x y) (print x "and" y "are equal")) ((< x y) (print x "is smaller than" y)) ((> x y) (print x "is bigger than" y))))
Don’t worry if this looks strange or hard to understand, at first. After some practice, you’ll be reading Lisp as easily as any other programming language. This article does not try to teach you everything about Hy. The goal is to get you familiar enough so that you can continue on your own.
An example program
The example program will be a simple phone book application that can track people and their phone numbers. If you want to follow along, you can type the example code as you read into a file called register.hy. The order of code is often not that important, but the import form should be at the top and the main entry point should be at the bottom. There is also some Python code. You do not need to type that into the file; it is there to help understand how Hy works compared to Python.
Unlike Python, Hy is not particular about indentation. However, since there are a lot more parentheses around than normal, it is a good idea to group the code nicely and in a way that it is easily read. Tools like Emacs will be able to do this for you, automatically.
Getting started with the database
An integral part of our phone book application is the database in which the data will be stored. Since we are using Sqlite, creating a database, connecting it and creating the needed schema is easy. The first step is to import the sqlite3 module so that we can call functions defined in it.
The next step is to define a few functions that we can later use to connect to the database and create the table where the data will be stored. The defn form is used to define a function. The first parameter is the name of the function to be defined, the second is a vector containing the parameters, and the rest is the actual code of the function.
(import sqlite3) (import sys) (defn get-connection [] (.connect sqlite3 "register.db"))
If we write the same code in Python, it would read as shown below. Notice how similar the code is to the Hy version of it. While we need to have a return statement in Python code in order to return a value to the caller, this is not needed in Hy. The value of the last expression evaluated is automatically returned.
import sqlite3 import sys def get_connection(): return sqlite3.connect("register.db")
Now that we have a function to connect to a database, lets write a function to create a database table as shown in the following example. The function takes a single parameter connection and uses it to execute the SQL statement in the Sqlite database. The last step is to return the connection:
(defn create-schema [connection] (.execute connection "create table if not exists person (name text not null, phone text)") connection)
If the code had been written in Python, it could read as follows:
def create_schema(connection): connection.execute("create table if not exists person (name text not null, phone text)") return connection
Manipulating the data
There are several ways in which to manipulate our data: create, read, update and delete. Lets write a function for each of these operations, starting from creating a new person. Because our person has only very few attributes and no methods, we do not create a class for this person. Instead, well pass around dictionaries and use keywords as keys. This is easier and requires less code than creating a class. So whenever you see code like (:name person) you know that we are getting the :name value from a person dictionary.
(defn insert-person [connection person] (let [[params (, (:name person) (:number person))]] (try (with [connection] (.execute connection "insert into person (name, phone) values (?, ?)" params)) (catch [e Exception] (print e) (print "failed to add a person")))))
This function takes two parameters: the connection to the database and the dictionary containing values of the person. Lets use the let form to bind a tuple (name and number) into the params variable. You could think of it as a local variable, since it is accessible only within the let form and ceases to exist when the execution flow leaves the form. The final step is to instruct the connection to execute the SQL-statement and supply parameters to it.
Error handling is done with try-catch block. If the code inside try-block throws an exception, the execution immediately skips to catch-block where the error is reported. The with-form takes advantage of the context manager provided by the SQLite connection. When execution enters the with-block, a database transaction is automatically started. If no exceptions occur, the transaction is committed when the with-block ends and changes are saved into the database. However, if there is an exception, the transaction is automatically rolled back and execution continues from the catch-block.
After the data has been saved, it would be nice to be able to load it too. Lets write three functions for this. One is used to load a single person by ID. The second function is a general search that can load multiple persons in a single query. The third is a function that turns a database row into a person dictionary:
(defn load-person [connection person-id] (row-to-person (.fetchone (.execute connection "select OID, name, phone from person where OID=?" (, person-id))))) (defn query-person [connection search-criteria] (let [[search-term (+ "%" search-criteria "%")] [search-param (, search-term search-term)]] (list-comp (row-to-person row) [row (.fetchall (.execute connection "select OID, name, phone from person where name like ? or phone like ?" search-param))]))) (defn row-to-person [row] {:id (get row 0) :name (get row 1) :number (get row 2)})
The query-person function introduces a new construct list-comp, which is used to perform list comprehensions. In the example, we are executing a sql query and fetching all matching records from the database. After that, we iterate over each row and call the row-to-person function for each of them. The results are collected together and returned as a list of dictionaries containing persons.
In row-to-person, a single database row is transformed into a dictionary. We are using keywords (:id, :name and :number) as keys, since it makes it easier to locate the data in the dictionary.
Update and delete functions are shown below. They are built using the same functions and constructs as the functions shown previously:
(defn update-person [connection person] (let [[params (, (:name person) (:number person) (:id person))]] (try (with [connection] (.execute connection "update person set name=?, phone=? where OID=?" params)) (catch [e Exception] (print e) (print "failed to update person"))))) (defn delete-person [connection person-id] (try (with [connection] (.execute connection "delete from person where OID=?" (, person-id))) (catch [e Exception] (print e) (print "failed to delete person"))))
User interface
The last step in our programming task is to tie everything together and write a user interface. I chose to write a text-based interface, since it is short and works on multiple platforms. Because the program supports both Python 2 and 3, well define a new function called key-input and, depending on the Python version, assign the input or raw input function into it. The if form takes three parameters and the last one is optional. The first parameter is the test being performed. The second part is executed if the test evaluates True; otherwise, the last block is executed:
(if (= (get sys.version-info 0) 3) (def key-input input) (def key-input raw-input))
Adding a person is a short function. First lets ask for the name and the phone number from the user and create a dictionary to represent a person. The dictionary is then passed to the insert-person function, which takes care of saving the person. The last step is to return True to inform the main loop that the user does not wish to quit yet:
(defn add-person [connection] (print "********************") (print " add person") (print "") (let [[person-name (key-input "enter name: ")] [phone-number (key-input "enter phone number: ")]] (insert-person connection {:name person-name :number phone-number :id None}) True))
To display a person, lets write a function that simply takes a person dictionary and outputs it on screen. The display-person function is then used when the user wishes to search entries in the database.
To search for data from the database, we have the function, search-person. It asks the user for text, be it a name or phone number, and uses it to search from the database. Here we are using a new formfor. For’ takes two parameters: an element, which is a collection pair in a form of lists or vectors, and a function to perform to each and every element. For does not return anything, and results returned from the function are discarded:
(defn display-person [person] (print (:id person) (:name person) (:number person))) (defn search-person [connection] (print "********************") (print " search person") (print "") (let [[search-criteria (key-input "enter name or phone number: ")]] (for (person (query-person connection search-criteria)) (display-person person))) True)
Editing a person is a somewhat more involved function. First, we load a person using an ID number that the user gives. If the ID is incorrect and not found in the database, the program notifies the user and returns to the main menu. However, if the person is actually found, the name and phone number are shown on screen. After this, the user can update the data and save it into the database again.
The code takes advantage of truth values. Python and Hy, in turn, have a convention where None, False, zero and an empty sequence or mapping are considered false, while any other value is considered true. The function tries to load the person by a given ID and only if it is found, continues into editing. Likewise, when a user simply presses Enter to give a name or phone number, the code detects this and uses the old value instead:
(defn edit-person [connection] (print "********************") (print " edit person") (print "") (let [[person-id (key-input "enter id of person to edit: ")] [person (load-person connection person-id)]] (if person (do (print "found person") (display-person person) (let [[new-name (key-input "enter new name or press enter: ")] [new-number (key-input "enter new phone or press enter: ")] [edited-person {:id (:id person) :name (if new-name new-name (:name person)) :number (if new-number new-number (:number person))}]] (update-person connection edited-person))) (print "could not find a person with that id")) True))
Compared to editing, removing a person is a rather simple operation. The code asks for the ID of the person to be removed and calls an appropriate function to perform the removal:
(defn remove-person [connection] (print "********************") (print " delete person") (print "") (let [[person-id (key-input "enter id of person to delete: ")]] (delete-person connection person-id)) True)
The function for ending the program closes the database connection and returns False. This causes the main loop to exit and the program to terminate:
(defn quit [connection] (.close connection) False)
Tying it together in the main menu
Finally, add the following code to create a main menu. Here, we are using a dictionary to map the numbers a user may enter to functions that should be executed. In case the user enters something that we don’t understand, we provide short instructions on how to operate:
(defn main-menu [connection] (let [[menu-choices {"1" add-person "2" search-person "3" edit-person "4" remove-person "5" quit}]] (print "********************") (print " register") (print "") (print "1. add new person") (print "2. search") (print "3. edit person") (print "4. delete person") (print "5. quit") (print "") (try (let [[selection (get menu-choices (key-input "make a selection: "))]] (selection connection)) (catch [e KeyError] (print "Please choose between 1 and 5") True)))) (if (= __name__ "__main__") (let [[connection (create-schema (get-connection))]] (while (main-menu connection) [])))
Here we are checking the __name__ attribute and if it is __main__, we know that the module has been started from the command line as a program and not included as a part of some other program. If this is the case, we call the functions we defined earlier in order to create our database and display the main menu. The while loop keeps running as long as the conditional evaluates to true. And since the value of the conditional is the same as the value returned by our functions, the program finishes as the quit function returns false.
You can try out the program at this point by issuing the following command in the command line:
hy register.hy
As a result, there should be a new file in the folder called register.db and the main menu is shown in the terminal. You can now add, search, edit and remove people and their phone numbers in your personal phonebook program.
What next
Hy is a young language and thus changing. It is possible to use existing Lisp tutorials, but not every feature mentioned in them is currently available in Hy, and some of the features might work slightly differently. However, the basic idea behind the language is still the same. And you already have a working phonebook program at your disposal. Why not try and add a new field for storing addresses or sort the search results by the name of the person, instead of the ID number.
If you are using emacs for writing programs, there is Hy mode available for it at: https://github.com/hylang/hy-mode. Only emacs24 and newer are supported, as of writing this article.
Note:
Since Hy is a relatively young language, it is evolving very fast. This article assumes that the version used is 0.9.10.
Example code in this article is available as a zip archive at https://github.com/tuturto/register/zipball/master.
For more information about Hy, take a look at http://docs.hylang.org/en/latest/.
Hy, like Python, has an interactive interpreter. You can start it by typing Hy in the terminal and close it by pressing Ctrl+D.
Developers of Hy and people interested in the language collaborate in #hy on irc.freenode.net.
what are the advantages/disadvantages compared to python ? does it work with python 3 or only python 2 ?
Helluw. One of the core devs here.
We currently support everything from 2.6 to 3.4. Pypy is also supported. There isnt any real disadvantages except for the fact it is still young. The advantages comes with the fact its Lisp, you got all the goodies like simple syntax and macros.
One fun advantage worth noting is that Hy code is more portable then Python code. Any Hy code you write will run on all versions of Python starting from 2.6.
sounds fun! will definitely try
Morten answered to your questions already, but I wanted to mention that the biggest advantage for me is the ability to call Python code from Hy and vice versa. I’m working on a rogue-clone that is written with Python (with user interface using Qt) and I used Hy to write the artificial intelligence routines.