-
Notifications
You must be signed in to change notification settings - Fork 344
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.