Skip to content

Haskell Interactive Mode Architecture

Chris Done edited this page May 10, 2014 · 4 revisions

This page is about the architecture of Haskell Interactive Mode, which should be helpful for curious users and prospective hackers.

How it works

Your Emacs will contain multiple sessions: one for each Haskell project you're working on. Each session is associated with a connected GHCi process and a REPL buffer.

             Emacs
            /     \
          /         \
    Session          Session
   /       \         /     \
Process  REPL     Process   REPL

When you open a Haskell file in a fresh project directory, it will prompt to start a new session for the enclosing directory containing a .cabal file. It will also start a GHCi process and open up an associated REPL buffer.

Command Protocol

There is a command queue which all communication with the GHCi process must go through:

Module 1 -> |*|
            | |
Module 2 -> |*|
            \_/---------> Process
                            |
Module 3  <--------*--------'

Only one command can run at once, anything else is queued up.

To queue a command you have two functions:

  • haskell-process-queue-command — queue a command to be run, asynchonrously, at some point. It's a FIFO queue.
  • haskell-process-queue-sync-request — queue a command and force all existing commands to complete and block on the result, returning a string output.

The first function takes a process as first argument, and a command as second. The command structure is here:

(defstruct haskell-command
  "Data structure representing a command to be executed when with
  a custom state and three callback."
  ;; hold the custom command state
  ;; state :: a
  state
  ;; called when to execute a command
  ;; go :: a -> ()
  go
  ;; called whenever output was collected from the haskell process
  ;; live :: a -> Response -> Bool
  live
  ;; called when the output from the haskell process indicates that the command
  ;; is complete
  ;; complete :: a -> Response -> ()
  complete)

A good example of this is the function haskell-process-reload-devel-main:

(defun haskell-process-reload-devel-main ()
  "Reload the module `DevelMain' and then run
`DevelMain.update'. This is for doing live update of the code of
servers or GUI applications. Put your development version of the
program in `DevelMain', and define `update' to auto-start the
program on a new thread, and use the `foreign-store' package to
access the running context across :load/:reloads in GHCi."
  (interactive)
  (with-current-buffer (get-buffer "DevelMain.hs")
    (let ((session (haskell-session)))
      (let ((process (haskell-process)))
        (haskell-process-queue-command
         process
         (make-haskell-command
          :state (list :session session
                       :process process
                       :buffer (current-buffer))
          :go (lambda (state)
                (haskell-process-send-string (plist-get state ':process)
                                             ":l DevelMain"))
          :live (lambda (state buffer)
                  (haskell-process-live-build (plist-get state ':process)
                                              buffer
                                              nil))
          :complete (lambda (state response)
                      (haskell-process-load-complete
                       (plist-get state ':session)
                       (plist-get state ':process)
                       response
                       nil
                       (plist-get state ':buffer)
                       (lambda (ok)
                         (when ok
                           (haskell-process-queue-without-filters
                            (haskell-process)
                            "DevelMain.update")
                           (message "DevelMain updated.")))))))))))

Code architecture

This mode consists of a few modules:

REPL Interaction

The prompt is set to EOT (end-of-transmission), which makes it easier to match when a command is completed and avoid interspersing with a prompt like Prelude> . The following interaction

λ> 1
1
λ>

In the background (and you can view this in the buffer *haskell-process-log*) the communication is actually:

-> "1
"
<- "1
"
<- "^D"

^D is the notation Emacs uses to refer to codepoint \4. M-x describe-char shows:

Character code properties: customize what to show
  name: <control>
  old-name: END OF TRANSMISSION
  general-category: Cc (Other, Control)
  decomposition: (4) ('')

So, when you're looking in the log, that's what that character is.

Clone this wiki locally