-
Notifications
You must be signed in to change notification settings - Fork 0
Haskell Interactive Mode Architecture
This page is about the architecture of Haskell Interactive Mode, which should be helpful for curious users and prospective hackers.
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.
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.")))))))))))This mode consists of a few modules:
- Sessions: haskell-session.el
- Processes: haskell-process.el
- REPL: haskell-interactive-mode.el
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.