Fork me on GitHub

Learn a Lisp with Python

It's been a long time since I've been wanting to learn a Lisp, probably a few years ago after becoming an Emacs user but that's another story... I wrote a little bit of elisp and clisp over the years for the sake of trying them but never had a strong motivation or need to learn them and it stayed like that until I discovered Hy.

This time around teaching myself a Lisp is not only really fun but also conveniently familiar as it has a Python back-end I already know and the standard Python modules I regularly use.

Yeah, it runs in Python so you can run Python from it!

(import MockSSH)


(defclass command-en [MockSSH.SSHCommand]
  "Prompt for and validate a password"
  [[start
    (fn [self]
      (setv self.password (bytes ""))
      (setv self.required_password (bytes "1234"))
      (.write self (bytes "Password: "))
      (setv self.callbacks [self.validate-password]))]

   [lineReceived
    (fn [self line]
      (setv self.password (bytes line))
      ((first self.callbacks)))]

   [validate-password
    (fn [self]
      (if (!= self.password "")
        (do
         (if (= self.password self.required_password)
           (do
            (setv self.protocol.prompt (bytes "hostname#"))
            (setv self.protocol.password_input False)))
         (if (!= self.password self.required_password)
           (do
            (.writeln self (bytes (.join " " ["MockSSH: password is" self.required_password])))
            (setv self.protocol.password_input False)))
         (.exit self))))]])

(setv commands {"en" command-en})
(setv prompt (bytes "hostname>"))
(setv keypath ".")
(setv interface "127.0.0.1")
(setv port 2222)
(setv users {"testuser" "1234"})
(apply MockSSH.runServer
       [commands prompt keypath interface port] users)

Here is the previous code before it was translated to Hy:

import MockSSH


users = {'testuser': '1234'}

class command_en(MockSSH.SSHCommand):
    """Prompt for and validate a password
    """
    def start(self):
        self.this_password = "1234"
        self.password = ""
        self.write("Password: ")
        self.protocol.password_input = True
        self.callbacks = [self.validatePassword]

    def lineReceived(self, line):
        self.password = line.strip()
        self.callbacks.pop(0)()

    def validatePassword(self):
        if self.password:
            if self.password == self.this_password:
                self.protocol.prompt = "hostname#"
                self.protocol.password_input = False
            else:
                self.writeln("MockSSH: password is %s" % self.this_password)
                self.protocol.password_input = False
            self.exit()

commands = {'en': command_en}

MockSSH.runServer(commands,
                  prompt="hostname>",
                  interface='127.0.0.1',
                  port=2222,
                  **users)

Looking at these examples I still felt like Python's syntax was lighter but comparing two pieces of code, that do the same thing, in different languages, and using the same module to do it forced me to think it through and see things from a different perspective, which is pretty cool.

In fact seeing how bad it looked in Hy gave me a much better view of how bad the Python implementation really was and although I never really liked it, I never really saw until now in how many ways it could be simplified... so I started moving things around in it and brought the previous code, to this:

(import MockSSH)

(setv users {"testuser" "1234"})

(defn mock-ssh [users commands host port prompt keypath] ;; replace with optional arguments
  (apply MockSSH.runServer
         [commands (bytes prompt) keypath (bytes host) port] users))

(defn en-change-protocol-prompt [instance]
  (setv instance.protocol.prompt (bytes "hostname#")))

(defn en-write_password_to_transport [instance]
  (.writeln instance (bytes (.join "" ["MockSSH: password is `" instance.valid_password "'"]))))

(setv password-prompting-command
      (apply MockSSH.PasswordPromptingCommand []
             {"password" (bytes "1234")
              "password_prompt" (bytes "Password: ")
              "success_callbacks" [en-change-protocol-prompt]
              "failure_callbacks" [en-write-password-to-transport]}))

(setv commands {"en" password-prompting-command
                "enable" password-prompting-command})

(apply mock-ssh [] {"users" users
                    "commands" commands
                    "host" "127.0.0.1"
                    "port" 2222
                    "prompt" "hostname>"
                    "keypath" "."})

Granted, most changes where done in the Python module, but they were motivated by wanting to improve the readability of the Lisp using it. This is what the Python equivalent now looks like:

import MockSSH


users = {'testuser': '1234'}

def en_change_protocol_prompt(instance):
    instance.protocol.prompt = "hostname #"
    instance.protocol.password_input = False

def en_write_password_to_transport(instance):
    instance.writeln("MockSSH: password is %s" % instance.valid_password)

command_en = MockSSH.PasswordPromptingCommand(
    password='1234',
    password_prompt="Password: ",
    success_callbacks=[en_change_protocol_prompt],
    failure_callbacks=[en_write_password_to_transport])

commands = {
    'en': command_en,
    'enable': command_en,
}

MockSSH.runServer(commands,
                  prompt="hostname>",
                  interface='127.0.0.1',
                  port=2222,
                  **users)

Many things in this language makes me feel like I'm learning to program all over again and I still haven't grasped many concepts of using a lisp but it's been very insightful to rethink the way I'm thinking when designing programs and sometimes even simple loops sometimes.

That said, my goal in using Hy was ultimately to make using the MockSSH module not require writing subclasses with programming logic in them in order to provide command emulation via its ssh server, and here's the final result:

(import MockSSH)
(require mockssh.language)


(mock-ssh :users {"testuser" "1234"}
          :host "127.0.0.1"
          :port 2222
          :prompt "hostname>"
          :commands [
  (command :name "en"
           :type "prompt"
           :output "Password: "
           :required-input "1234"
           :on-success ["prompt" "hostname#"]
           :on-failure ["write" "MockSSH: Password is 1234"])])
$ hy mock.hy
Listening on 127.0.0.1 port 2222...
$ ssh testuser@0 -p 2222
testuser@0's password:
hostname>en
Password:
Pass is 1234...
hostname>en
Password:
hostname#

You can find all the code behind this tiny DSL on github.

If you want to learn more about how Hy is implemented and runs in python, watch the recorded talk.

Thanks to Paul Tagliamonte for presenting at Pycon 2014!

blogroll

social