Sabela is a reactive notebook environment for Haskell. The name is derived from the Ndebele word meaning "to respond." The project has two purposes. Firstly, it is an attempt to design and create a modern Haskell notebook where reactivity is a first class concern. Secondly, it is an experiment ground for package/environment management in Haskell notebooks (a significant pain point in IHaskell).
git clone https://github.com/DataHaskell/sabela
cd sabela
cabal update
cabal runOpen localhost:3000/index.html and explore either:
./examples/analysis.mdfor a quick tutorial.- or click on the book icon on the top left for some ready to use snippets.
The execution and dependency management model is based on scripths.
Sabela is aimed at exploratory Haskell work where you want:
- regular Haskell code, not a special notebook language
- reactive reruns when upstream cells change
- package directives via
-- cabal:metadata - Markdown prose mixed with executable Haskell
- rich output in the browser
- a file explorer and save/load workflow for
.mdnotebooks
Clone the repo and start the server:
git clone https://github.com/DataHaskell/sabela
cd sabela
cabal update
cabal runThen open:
http://localhost:3000/index.html
By default, Sabela serves the UI from static/ and uses the current working directory as the file explorer root.
You can also pass explicit arguments:
cabal run sabela -- 3000 static .The CLI shape is:
sabela [port] [static-dir] [work-dir]
A Sabela notebook is just a Markdown file containing prose plus fenced Haskell code blocks.
For example:
# My first Sabela notebook
This is prose.
```haskell
x = 10
```
More prose.
```haskell
print (x + 5)
```When Sabela loads a notebook:
- prose sections become prose cells
- fenced code blocks become code cells
- running the notebook executes the code cells in order
When you save, Sabela writes the current notebook state back out as Markdown again.
That means notebooks stay readable in Git, easy to diff, and editable outside the website.
Create a file called examples/tutorial.md:
# Sabela basics
This notebook shows the core reactive workflow.
```haskell
x = 10
```
```haskell
y = 20
```
```haskell
print (x + y)
```Now change the first cell from:
x = 10to:
x = 100Sabela tracks definitions and uses heuristically, so when an upstream cell changes, downstream cells that depend on those names are rerun automatically.
In this example, the final cell should update from 30 to 120.
This is the central Sabela workflow:
- define values in small cells
- compose later cells from earlier ones
- edit upstream definitions
- let the notebook rerun affected dependents
Sabela does not currently build a full Haskell dependency graph. Instead, it uses a lightweight textual approximation:
- it scans each code cell for names it appears to define
- it scans for names it appears to use
- when a cell is edited, later code cells are rerun if they use names defined by changed cells
This approach is simple and fast, and it works well for many didactic and exploratory notebooks.
You should still understand its limits:
- it is order-sensitive
- it is heuristic, not compiler-accurate
- unusual syntax may not be tracked perfectly
- circular dependencies are not deeply modeled yet
So the best style for Sabela notebooks is:
- keep cells small
- define values and helper functions clearly
- prefer a top-to-bottom narrative order
Anything that works in GHCi generally fits naturally in a Sabela code cell.
Example:
let triples =
[ (a,b,c)
| c <- [1..20]
, b <- [1..c]
, a <- [1..b]
, a*a + b*b == c*c
]
print triplesSabela ships with a small gallery of built-in examples in the UI, including basics, library usage, display examples, concurrency, QuickCheck, and file I/O.
One of Sabela’s most important ideas is that notebook package requirements live inside the notebook itself.
You do this with -- cabal: metadata at the top of a code cell.
Example:
-- cabal: build-depends: text
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
let msg = T.pack "Hello, Sabela!"
TIO.putStrLn (T.toUpper msg)You can also declare extensions:
-- cabal: build-depends: aeson, text, bytestring
-- cabal: default-extensions: DeriveGeneric, OverloadedStringsSabela scans all code cells, merges their -- cabal: metadata, and computes the full required package/extension set for the notebook.
If the dependency set changes, Sabela:
- resolves or updates the package environment
- restarts the GHCi session
- injects its display helper prelude
- reruns the relevant cells
That means dependencies are notebook-level in effect, even though the directives are written in cells.
A practical tip: put your main dependency directives near the top of the notebook so the environment story is easy to read.
Note: Sabela curently only supports exact versions - not the cabal package range syntax.
That is: dataframe-0.5.0.0 will work but dataframe <= 1 won't.
Sabela injects helper functions into the GHCi session so cells can emit structured browser output.
The key helpers are:
displayHtmldisplayMarkdowndisplaySvgdisplayLatexdisplayJsondisplayImage
If you just print something, it is treated as plain text.
Note: rich output text must be the only thing output in the cell.
displayMarkdown $ unlines
[ "# Analysis Results"
, ""
, "The computation found **42** as the answer."
, ""
, "| Metric | Value |"
, "|--------|-------|"
, "| Speed | Fast |"
, "| Memory | Low |"
]displayHtml $ unlines
[ "<h2>Hello from Sabela</h2>"
, "<p>This is <strong>rich HTML</strong> output.</p>"
, "<ul><li>Item one</li><li>Item two</li></ul>"
]-- cabal: build-depends: text, granite
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as T
import Granite.Svg
displaySvg $ T.unpack
(bars [("Q1",12),("Q2",18),("Q3",9),("Q4",15)] defPlot { plotTitle = "Sales" })These helpers work by prefixing output with a MIME marker that the server parses before sending results to the browser.
Here is a complete small notebook you can drop into examples/minimal.md.
# A tiny Sabela notebook
This notebook demonstrates reactivity, Markdown output, and an SVG plot.
```haskell
numbers = [1..10]
```
```haskell
squares = map (^ (2 :: Int)) numbers
```
```haskell
print squares
```
```haskell
displayMarkdown $ unlines
[ "# Summary"
, ""
, "We computed **squares** for the numbers " ++ show (head numbers) ++ " through " ++ show (last numbers) ++ "."
, ""
, "- Count: " ++ show (last numbers)
, "- Max: " ++ show (last squares)
]
```Edit numbers, and the downstream cells should update.
The repository includes a larger example at examples/analysis.md based on the California housing dataset.
That notebook shows a very good real Sabela workflow:
- load data with
DataFrame - inspect rows
- compute summaries
- get categorical frequencies
- plot histograms
- create derived features
- declare typed column references with Template Haskell
- visualize spatial structure with scatter plots
- compute correlations against a target variable
Sabela also provides IDE-style help for the active notebook session.
The UI exposes a lookup panel, and the backend can query GHCi for:
- completions
:info:type:doc
So once your notebook has loaded modules and defined names, you can inspect them from the same live session.
This is especially useful for exploratory work where you are mixing notebook execution with interactive discovery.
Sabela includes a file explorer rooted at the configured working directory.
That gives you a nice workflow for:
- opening existing Markdown notebooks
- creating new files and directories
- reading and editing files
- saving notebooks back to disk
A useful convention is something like:
examples/
basics.md
dataframe_intro.md
plotting.md
california_housing.md
Since notebooks are plain Markdown, they work very naturally with Git and code review.
When a cell fails, Sabela captures stderr from GHCi and parses error locations into structured cell errors.
In practice, that means:
- ordinary compile/runtime messages still appear
- line/column information is surfaced when available
- fixing a broken upstream cell can automatically repair downstream cells on rerun
A few debugging tips:
- keep imports and dependency pragmas near the top
- isolate complicated definitions into their own cells
- prefer explicit helper names over deeply nested one-liners
- use
printfor plain debugging anddisplayMarkdown/displayHtmlfor presentation
The execution model is worth understanding because it explains most notebook behavior.
Sabela maintains a single GHCi session for the notebook.
When needed, it starts GHCi roughly with:
--interactive-ignore-dot-ghci- language extensions from notebook metadata
- package flags or package environment information derived from notebook metadata
For a given code cell, Sabela:
- parses the source as a script fragment
- renders it into GHCi-friendly script text
- sends the lines to the running session
- places a unique marker after the cell
- drains stdout until the marker appears
- collects stderr separately
- parses MIME markers and error information
- broadcasts the result to the frontend
This marker-based approach is the trick that lets Sabela separate one cell’s output from the next while still using a single long-lived GHCi process.
Sabela is already quite usable, but it is still an early system. A good tutorial should be honest about that.
Current constraints include:
- dependency tracking is heuristic rather than compiler-accurate
- cell scheduling is currently linear rather than topologically sorted
- circular dependencies are not deeply handled
- notebook semantics are tied to a single session model
- package environment changes require session restart
These are not necessarily flaws; many are reasonable early tradeoffs for a simple and understandable architecture.
If you want notebooks that feel clean and robust, use this style:
Start with:
- package directives
- extensions
- imports
- small helper functions
Use one cell per conceptual step:
- data loading
- inspection
- cleaning
- feature engineering
- plotting
- modeling
- interpretation
Prefer:
- named intermediate values
- small helper functions
- explicit imports
- prose between major steps
- for memory efficiency don't declare expensive things in top level variables.
Sabela works best when the notebook is both:
- executable
- readable as a document
That means prose should explain:
- what you are doing
- why you are doing it
- what the output means
- what the next cell will test
