ITADN
DataHaskell/sabela · 文件 下载 ZIP
文件最后提交记录最后更新时间
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

sabela

Sabela 是一个用于 Haskell 的响应式笔记本。其名称源自 Ndebele 语言中意为 响应 的词汇,这正是其核心设计理念。

A screenshot of the web ui

快速开始

git clone https://github.com/DataHaskell/sabela
cd sabela
cabal update
cabal run

打开 localhost:3000/index.html,然后:

  • 打开 examples/CaliforniaHousing.md 查看完整的示例,该示例 使用加州住房数据构建 Hasktorch 线性回归模型,或
  • 点击左上角的书本图标,查看可直接运行的代码片段。

该文件夹中包含展示不同功能的示例,例如 widgets 以及 Python 集成。

其执行和依赖模型基于 scripths

教程


1. 安装并运行

git clone https://github.com/DataHaskell/sabela
cd sabela
cabal update
cabal run

然后打开 http://localhost:3000/index.html。默认情况下,文件资源管理器 以当前目录为根目录。

要传递选项,参数顺序为:

sabela [port] [work-dir] [global-file] [packages...]

因此,要在不同的端口上运行,并使用您自己的 notebook 目录:

cabal run sabela -- 8080 ~/notebooks

global-file 默认为 ~/.sabela/global.md(参见 第 6 节]);任何 尾部参数均为要预安装的额外软件包。


2. 笔记本模型

笔记本是一个普通的 Markdown 文件:散文加上带围栏的 Haskell 代码块。

# My first notebook

This is prose.

```haskell
x = 10
```

More prose.

```haskell
print (x + 5)
```

带围栏的代码块作为代码单元格加载,它们之间的文本作为散文单元格加载。保存会将笔记本重新写回为 Markdown,因此可以在 Git 中干净地进行差异比较,并可在浏览器外正常编辑。


3. 你的第一个响应式笔记本

创建一个文件 examples/tutorial.md:

# Sabela basics

```haskell
x = 10
```

```haskell
y = 20
```

```haskell
print (x + y)
```

4. 响应式机制如何工作

当你编辑一个单元格时,Sabela 会使用 ghc-lib-parser 解析每个单元格, 这是一个 GHC 自身解析器的独立副本,无论你的 GHC 版本如何都能正常工作。 从语法树中,它读取每个单元格定义的顶层名称,以及每个单元格使用但未自行绑定的名称。 这提供了一个真实的依赖图,而不是基于文本匹配的猜测。

编辑一个单元格会按依赖顺序重新运行其依赖项,该顺序由拓扑排序计算得出, 而非基于笔记本中的位置,因此每个值都会在读取它的单元格之前重新计算。 有两种情况会被报告而不是执行:

  • 重新定义。 第一个定义某个名称的单元格拥有该名称。之后定义相同名称的单元格 会被标记为错误,并指向原始定义,而不是默默地遮蔽它。
  • 循环。 如果两个单元格相互依赖,两者都会被报告为循环,而不是陷入死循环。

响应式语义中仍然存在一些缺口,例如类型类定义的变化 目前尚未传播,但常见情况已得到覆盖。


5. 运行纯 Haskell

任何能在 GHCi 中运行的内容都能在单元格中运行:

let triples =
      [ (a, b, c)
      | c <- [1..20]
      , b <- [1..c]
      , a <- [1..b]
      , a*a + b*b == c*c
      ]

print triples

6. 在单元格中添加包依赖

笔记本自带其包需求。你在单元格顶部使用 -- cabal: 指令来声明它们:

-- 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)

扩展也遵循同样的方式:

-- cabal: build-depends: aeson, text, bytestring
-- cabal: default-extensions: DeriveGeneric, OverloadedStrings

指令是按单元格划分的,但它们适用于整个笔记本:Sabela 会将所有单元格中的 -- cabal: 行合并为一个包集合。当该集合发生变化时,它会解析包环境,重启 GHCi,重新注入显示辅助函数,并重新运行需要这些更改的单元格。将主要指令放在顶部附近,可以使环境更易于阅读。

对于希望在每个笔记本中都使用的依赖项,请将相同的指令放入 global.md(默认为 ~/.sabela/global.md)。

编译单元格(-- compile

与其他笔记本不同,还可以编译包含繁重计算的单元格:

```haskell
-- compile
trainEpoch :: Model -> [Batch] -> (Model, Double)
trainEpoch m bs = ...
```

```haskell
trainEpoch model batches   -- 调用原生 -O2 代码
```
  1. 编译单元格仅包含在模块顶部合法的内容: imports、类型签名、绑定、data/class/instance 声明。
  2. 下游单元格直接使用编译后的名称,无需导入。
  3. 跨单元格的顶层名称重复是一个错误(编译定义 不能被遮蔽),并且编译单元格不能使用在解释 单元格中定义的名称。

-- compile: Training 显式命名生成的模块;共享 名称的单元格合并为一个模块(这也允许它们之间 相互递归),当一个模块使用另一个模块的名称时,模块之间会自动相互导入。使用单元格操作中的 ⚡ 按钮来切换 指令,而无需手动输入。


7. 富输出辅助函数

Sabela 将显示辅助函数注入到会话中,以便单元格可以输出结构化内容而非纯文本:

辅助函数输出
displayHtmlHTML
displayMarkdownMarkdown
displaySvgSVG
displayLatexLaTeX
displayJsonJSON
displayImagebase64 图像(接受 MIME 类型和数据)

普通的 print 显示为文本。富输出必须是单元格打印的唯一内容。

Markdown

displayMarkdown $ unlines
  [ "# Analysis Results"
  , ""
  , "The computation found **42** as the answer."
  , ""
  , "| Metric | Value |"
  , "|--------|-------|"
  , "| Speed  | Fast  |"
  , "| Memory | Low   |"
  ]

HTML

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>"
  ]

来自 notebook 工作目录的文件通过 /api/asset?path=<path> 提供,并可在 HTML 中使用。

SVG

-- 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" })

每个辅助函数都会在其输出前添加一个 MIME 标记,服务器在将结果发送到浏览器之前会将其去除。


8. 交互式小部件

小部件是一种 HTML 控件(滑块、下拉框、复选框、文本框、按钮), 它存在于单元格的输出中,并在你操作它时重新运行该单元格。你无需编写 JavaScript:Sabela 负责渲染控件,并将其值传回 GHCi 以 重新运行单元格。

每个小部件都是一个 Behavior a:一个知道如何渲染自身并 采样其当前值的值。动词 display 同时执行这两项操作:它绘制控件 并返回该值:

c <- display (slider "celsius" (20 :: Int) (-40) 120)
let f = c * 9 `div` 5 + 32

displayHtml $ "<p><b>" ++ show c ++ " °C</b> = " ++ show f ++ " °F</p>"

拖动滑块,单元格会以新的 c 重新运行。任何下游使用 c 的单元格也会重新运行,因为小部件值是一个普通的 Haskell 绑定。

完整集合:

小部件返回值
slider name def lo hi拖动时的数值(防抖)
dropdown name options def选定的 String
checkbox name def一个 Bool
textInput name def当前的 String
button label nameMaybe ()(点击一次后为 Just ()
scatterSelect name points[Int],套索选中点的索引

BehaviorApplicative,因此可以将小部件组合为一个值:

area <- display (liftA2 (*) (slider "w" (10 :: Int) 1 100)
                            (slider "h" (10 :: Int) 1 100))
displayHtml $ "<p>Area: <b>" ++ show area ++ "</b></p>"

用 Lasso 绘制散点图

scatterSelect 会绘制一个 HTML5 canvas 并返回你所圈选点的索引。将这些索引传递给行过滤器,下游单元格将精确显示你所选中的行:

sel <- display (scatterSelect "districts" [(lon, lat) | (lon, lat) <- coords])

sel

9. 在同一笔记本中使用 Python

在单元格的边栏语言下拉菜单中选择 py,该单元格将在持久的 Python REPL 中运行,而不是 GHCi。变量会在笔记本中持续存在,并且相同的 displayHtml / displayMarkdown 辅助函数也可以使用。(你需要在 PATH 上安装 python3。)

def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

print([fib(i) for i in range(10)])

这两种语言运行在独立的进程中,因此它们通过桥接而非共享内存来传递值。从 Haskell 导出:

exportBridge "names" (show names)

并且该值以字符串 _bridge_names 的形式到达 Python:

import ast
names = ast.literal_eval(_bridge_names)

反过来也是如此:exportBridge("result", json.dumps(r)) 在 Python 中表现为 Haskell 中的 _bridge_result,而从 Python 导出会重新运行读取它的 Haskell 单元格。通常的做法是在 Haskell 中使用 DataFrame 加载并类型检查数据,将其作为 CSV 传输,然后让 pandas 或 matplotlib 接管。examples/tutorial-python-integration.mdexamples/matplotlib-demo.md 对此进行了讲解。


10. 使用 Claude Code 进行结对编程(siza 技能)

Sabela 通过位于 /api/ai/* 的小型 REST API 暴露其笔记本,而 siza 是一个驱动它的 Claude Code 技能。当你在浏览器中打开笔记本,并在第二个终端中安装 siza 后,Claude 可以列出单元格、读取它们、运行它们、提出你在 UI 中批准的编辑,并使用 try 操作针对笔记本上下文和候选依赖项进行非提交式实验。

在 Claude Code 内部安装它:

/plugin marketplace add /path/to/sabela/cli-skill
/plugin install siza
/reload-plugins

然后启动 Sabela(cabal run),打开一个 notebook,并向 Claude 询问诸如 "我的 notebook 里有什么?""运行第 3 个单元格并告诉我它输出了什么",或者 "添加一个单元格,绘制中位数收入与房价的关系图。" siza 会自行找到 正在运行的服务器:Sabela 在启动时写入 ~/.local/state/sabela/servers/<port>.json 以便该 skill 可以读取它。

在共享或远程机器上,使用 token 对 bridge 进行访问控制:

SABELA_AI_TOKEN=$(openssl rand -hex 16) cabal run

客户端随后发送 Authorization: Bearer <token>;其余 UI 保持 打开状态。完整详情见 cli-skill/README.md

与本地模型配对(siza chat

siza 二进制文件还有一个 chat 子命令,用于通过 本地 Ollama 模型 驱动你的 notebook。 Siza 是一个已集成到 Haskell 包生态系统和 GHC 编译器中的 harness。该 harness 包含钩子,可对代码应用确定性修复, 执行上下文包搜索,并将响应式 notebook 语义嵌入为 LLM 可输出内容的约束。

前提条件:Ollama 正在运行且已拉取模型(例如 ollama pull gpt-oss:20b),并且 Sabela 服务器已启动(cabal run)。然后:

cabal run exe:siza -- chat                     # discovers the running server
cabal run exe:siza -- chat --model gemma4:latest --url http://localhost:3000
标志默认值含义
--modelgpt-oss:20bOllama 模型标签(必须与 ollama list 匹配)
--url已发现的服务器要驱动的 Sabela 服务器
--timeout300每个请求的墙钟时间上限(秒)
--max-turns40每个请求的最大 harness 轮数
--verbose关闭流式传输完整审计(系统提示、思考过程、工具 JSON)

名称解析(查找要导入的库函数)使用本地 Hoogle 缓存。 如果模型报告找不到函数,请从仓库中运行 make search-cache 构建一次缓存。


11. 展示与导出

笔记本不仅用于编辑。Sabela 在 /dashboard 处将同一笔记本作为实时 仪表盘 提供服务,在 /slideshow 处作为 幻灯片 提供服务,并且可以 导出为可交付的文件:

GET /api/export/…输出
markdown以纯 .md 格式的笔记本
dashboard / slideshow一个内置笔记本的独立 HTML 页面
haskell一个可运行的 .hs cabal 脚本,裁剪至单元格的依赖项
lhs文学式 Haskell
reactive一个无头 reactive-banana 程序

由于事实来源是 Markdown,最简单的导出方式就是直接保存 该文件。


12. 一个最小化的笔记本

将以下内容放入 examples/minimal.md 中,即可在一个文件中实现响应式、Markdown 输出和 SVG 绘图:

# A tiny Sabela notebook

```haskell
numbers = [1..10]
```

```haskell
squares = map (^ (2 :: Int)) numbers
```

```haskell
print squares
```

```haskell
displayMarkdown $ unlines
  [ "# Summary"
  , ""
  , "Squares for " ++ show (head numbers) ++ " through " ++ show (last numbers) ++ "."
  , ""
  , "- Count: " ++ show (last numbers)
  , "- Max: " ++ show (last squares)
  ]
```

编辑 numbers,其下方的三个单元格会随之更新。


13. 加州住房示例

examples/CaliforniaHousing.md 是一个关于加州 住房数据集的完整端到端笔记本。它涵盖了:

  • 使用 DataFrame 加载数据
  • 检查行并计算摘要
  • 类别频率和直方图
  • 工程化派生特征
  • 通过 Template Haskell 进行类型化列引用
  • 空间结构的散点图
  • 与目标变量的相关性
  • 训练 Hasktorch 线性回归模型
  • 在留出区上评估预测

如果你希望从更小的规模开始,examples/Iris.md 在 经典的 Iris 数据集上训练决策树,而 examples/grammar-of-graphics.md 使用 Granite 的图形语法 API 构建分层图表。


14. 从会话中查找名称

查找面板会查询实时会话以获取补全建议,:info:type:doc。一旦笔记本加载了其模块并定义了名称,你就可以 从正在运行单元格的同一会话中检查它们,这在你边使用边学习 API 时非常有用。


15. 文件、加载与保存

Sabela 的文件资源管理器以工作目录为根,因此你可以从 UI 中打开 现有的笔记本,创建文件和目录,并将内容保存回磁盘。由于笔记本是纯 Markdown,一种布局如下:

examples/
  basics.md
  plotting.md
  CaliforniaHousing.md

它只是磁盘上的文件,并且能够自然地与 Git 和代码审查配合使用。


16. 错误与调试

当单元格失败时,Sabela 会捕获 GHCi 的 stderr 并将其解析为结构化错误,在 GHCi 提供行和列信息的地方包含这些信息。修复上游损坏的单元格会在下次重新运行时修复其依赖项。

以下几点有所帮助:

  • 将 imports 和 -- cabal: 指令放在顶部附近
  • 为复杂的定义单独创建一个单元格
  • 优先使用命名辅助函数而非深度嵌套的单行代码
  • 使用 print 进行调试,使用 display* 辅助函数进行展示

17. Sabela 如何执行单元格

Sabela 为每个 notebook 维护一个长生命周期的 GHCi 进程,该进程通过 -ignore-dot-ghci 加上 notebook 元数据中的扩展和包环境启动。要执行一个单元格,它:

  1. 将源代码解析为脚本片段
  2. 将其渲染为 GHCi 脚本文本
  3. 将这些行发送到会话
  4. 在其后写入一个唯一标记
  5. 排空 stdout 直到标记出现
  6. 单独收集 stderr
  7. 解析出 MIME 标记和错误位置
  8. 将结果广播到前端

该标记是让一个长生命周期进程服务于整个 notebook 同时保持每个单元格输出独立的技巧。Python 单元格以相同的方式 针对其自身的 REPL 工作。


18. 当前限制

Sabela 是一个年轻的项目,存在一些不足之处。值得了解的约束条件:

  • 每种语言在 notebook 中运行于单个会话
  • 更改包环境会重启该会话
  • Haskell↔Python 桥接以字符串形式传递值,而非活动对象

19. 良好的笔记本风格

在以下情况下,笔记本的阅读体验最佳:

  • 将设置放在最前面:指令、扩展、导入和小型辅助函数置于顶部
  • 为每个步骤分配独立的单元格:加载、清洗、特征工程、 绘图、建模、解释
  • 为中间值命名,而不是将所有内容嵌套在一个表达式中
  • 避免在顶层绑定昂贵的值,以降低内存占用
  • 将文本作为文档来撰写:解释每个步骤的作用及其 输出的含义,使笔记本既易于阅读又易于运行