STATION IV · 上游CH 17里程 17/20

工程化:cabal、HLS 与真实项目

上游卷的最后一章,把「会语言」升级成「能开工」。你已经拥有对付任何构建系统的心智模型(Gradle 都伺候得了,cabal 只会更简单),所以本章走对照路线:每个概念旁边标注它在你世界里的对应物,然后解剖一棵真实项目树,最后给你一条「读懂 Hackage 上任何包」的路径——这是自学生态的万能钥匙。

cabalHackage模块HLS

项目骨架:cabal 文件 = build.gradle

cabal init 生成骨架,核心是一个 .cabal 声明文件,角色完全等同 build.gradle——只是纯声明、无脚本,读起来更像 TOML 而不像一门可编程构建 DSL,五分钟能看完全部字段:

name:               log-analyzer
version:            0.1.0.0

library                              -- 一个包 = library + 若干可执行 + 测试
  hs-source-dirs:   src
  exposed-modules:  Analyzer, Analyzer.Parser, Analyzer.Stats
  build-depends:    base >=4.18 && <5, text ^>=2.1, containers
  default-language: GHC2021          -- 语言版次,第 1 章提过
  ghc-options:      -Wall            -- 军规①:警告全开

executable log-analyzer
  main-is:          Main.hs
  hs-source-dirs:   app
  build-depends:    base, log-analyzer   -- 可执行文件依赖自家 library

test-suite spec
  type:             exitcode-stdio-1.0
  main-is:          Spec.hs
  hs-source-dirs:   test
  build-depends:    base, log-analyzer, QuickCheck, hspec

注意结构划分:library 装全部逻辑,executable 只是薄薄一层 main——第 11 章的纯核心脏外壳,在工程布局上再现了一次(app/Main.hs 通常十行以内);测试套件依赖的是 library,所以整湖逻辑天然可测。日常四连:cabal buildcabal runcabal testcabal repl(把整个项目加载进 GHCi——还记得 GHCi 驱动开发吗?这就是入口)。

依赖仓库叫 Hackage(= Maven Central),版本约束遵循 PVP 规范(^>=2.1 读作「2.1 系列,信任小版本」,精神同 semver);可复现构建用 cabal freeze 生成锁定文件进版本库,对应 gradle 的 lockfile。另一套并行方案 Stack + Stackage 提供「LTS 快照」:一大批包的版本组合经过 CI 验证互相兼容,你锁一个快照号就锁了全世界——团队项目求稳的常见选择。二选一不必纠结:概念 95% 重合,本书用 cabal,因为它是官方默认且近年已足够好用。

模块:一门语言的命名空间礼仪

module Analyzer.Parser
  ( LogLine(..)        -- 导出类型和它全部构造子
  , parseLine          -- 导出函数
  -- mkRaw 没写 = 私有。第 4 章的智能构造子就靠这个机制
  ) where

import Data.Map (Map)                 -- 类型直接引
import qualified Data.Map as Map      -- 函数带前缀:Map.lookup、Map.insert

两条惯例值得原样抄走。导出列表就是 API 合同:没列的名字外界不可见——Kotlin 的 internal、Java 的包私有,这里靠一张白纸黑字的清单,评审时看一眼 diff 就知道 API 动没动。qualified import 是社区礼仪:Data.Map、Data.Text 这些「函数名和 Prelude 撞车」的模块,一律带前缀用(Map.lookup),读代码的人永远不用猜「这个 lookup 是谁家的」。

工具链:补全、体检、整容

第 1 章装的 HLS(Haskell Language Server)配 VS Code 的 Haskell 插件即用:类型悬浮、跳转、重命名、case 自动补分支(演绎自第 3 章的类型洞)。再加两件:hlint(静态体检,还会教学——「你这个 if x then True else False 可以直接写 x」,相当于一个随身老 Haskeller)= ktlint/detekt;fourmolu(格式化,团队统一风格,commit 钩子跑)= ktfmt/prettier。军规清单顺手立在这里:-Wall 全开且 CI 里当错误、文本一律 Text 不用 String(第 11 章的账)、顶层函数必写签名(第 3 章的账)、部分函数(head/fromJust)禁用(第 4 章的账)——四条全是前文伏笔,一个 .cabal 文件收口。

解剖一棵真实项目树

log-analyzer/
├── log-analyzer.cabal        -- 声明文件(上文)
├── cabal.project             -- 多包工程/本地覆盖(≈ settings.gradle)
├── src/                      -- library:纯逻辑的湖
│   ├── Analyzer.hs           -- 门面模块,re-export 公共 API
│   └── Analyzer/
│       ├── Types.hs          -- ADT 全家(第 4 章)与 newtype
│       ├── Parser.hs         -- 组合子解析(第 14 章)
│       └── Stats.hs          -- 纯统计(第 5 章的 fold 们)
├── app/Main.hs               -- 脏外壳:读参数、readFile、打印,~15 行
└── test/Spec.hs              -- hspec 示例测试 + QuickCheck 性质(第 13 章)

这个形状你应该越看越眼熟——它就是全书路线的物化:类型定义一间房、解析一间房、纯逻辑一间房,IO 挤在门厅,测试和性质守在门口。真实的开源项目(去翻 pandoc 或 ShellCheck 的仓库)在同款骨架上按体量加层,但「湖 + 薄壳」的比例惊人地稳定。

最后是那把万能钥匙:怎么读懂 Hackage 上任何一个包。路径固定:① 包主页看 README 和依赖(依赖少的包好读);② 点进 Haddock 文档,先通读模块的导出列表和类型签名——第 3 章的技能,此刻满值回报,好的 Haskell 库光看签名能懂七成;③ 挑一个核心类型,点「Source」看定义(通常是个 ADT 或 newtype,第 4 章);④ 有疑问的函数,Hoogle 搜签名找同类比对。这条路径的前提是这门语言「签名不说谎」——所以它在 Haskell 生态的效率远高于在其他生态,请趁热用。

从零到 CI:最后一块拼图

项目要见人,还差文档和流水线。文档:Haddock 注释(-- | 开头)写在导出项上,cabal haddock 生成你在 Hackage 见到的那种页面——第 17 章四步读码法的第②步,现在你也是供给侧了。CI:GitHub Actions 有现成的 haskell-actions/setup,骨架短到可以贴全:

-- .github/workflows/ci.yml(要点版)
-- steps:
--   - uses: actions/checkout@v4
--   - uses: haskell-actions/setup@v2       # 装 GHC + cabal
--   - run: cabal build --enable-tests
--   - run: cabal test                       # hspec + QuickCheck 全家
--   - uses: actions/cache@v4                # 缓存 ~/.cabal 和 dist-newstyle
--     with: { path: dist-newstyle, key: ... }   # 没有这步,每次 CI 十分钟起

缓存那两行不是可选项——本章丑话里的编译速度税,在 CI 上靠缓存能砍掉八成。加上 hlint 和 fourmolu 的检查步骤,你就拥有了和 JVM 项目同规格的工程闭环:格式化统一、静态检查、测试门禁、增量构建,一个不缺。从 cabal init 到这条绿色流水线,一个下午足够——这门语言的工程化名声比实际水平差了十年,亲手跑一遍就知道了。

▸ 下游对照 · 工具链换算表
你的世界上游对应差异一句话
Gradle / Mavencabal / stack声明式更彻底,没有可编程 build 脚本
Maven CentralHackage(+ Stackage 快照)快照制是特色:整批依赖一起验兼容
ktlint / detekthlint建议自带重构讲解,可当老师用
ktfmt / prettierfourmolu / ormolu同款哲学:少配置,少吵架
Kotlin 语言服务HLS同为 LSP;类型洞与 case 补全是特产
JUnit + jqwikhspec/tasty + QuickCheck性质测试是一等公民不是插件
⚠ 暗礁 · 两句丑话

其一,「cabal hell」的历史名声:2010 年代依赖冲突确实地狱,但 v2-build(2018)按项目隔离构建产物后已大体成为往事;遇到旧文章教你 sandbox/全局装包,直接关掉,那是考古层。其二,编译速度是真慢:GHC 优化极重,大项目全量构建以十分钟计。缓解手段:开发期用 cabal repl + :r 增量加载(秒级)、ghcid 自动重载、CI 上缓存 dist-newstyle。诚实结论:编译慢是这门语言用类型和优化换来的持续税,习惯 Kotlin 增量编译的你要有心理预期——但也别忘了,你在 REPL 里的迭代速度反而比 JVM 世界快。

◆ 水文记录 · 语言扩展:GHC 的军械库文化

读真实项目你会看到文件头一排 {-# LANGUAGE OverloadedStrings #-}。这是 GHC 独有的文化:语言特性做成开关,项目按需启用,成熟一批就打包成版次(GHC2021 默认开了约 24 个,GHC2024 又收编一批)。高频几个混个脸熟:OverloadedStrings(字符串字面量可以是 Text)、LambdaCase(\case 省一个变量名)、DeriveFunctor/Generic(derive 更多能力)、StrictData(第 16 章)。心态放平:扩展不是「非标准黑魔法」,而是这门语言演化的正常通道——今天的扩展就是明天的标准。

✚ 动手泅渡

1. 真机实操:cabal init --interactive 建一个项目,把第 11 章的日志统计器(analyze + main)按「湖 + 薄壳」放进 src/ 和 app/,跑通 cabal run

2. 给它加测试套件:hspec 写两个示例测试,QuickCheck 写一条性质(第 13 章的作业正好搬来),跑通 cabal test

3. 读包练习:去 Hackage 找 base64-bytestring(小而精),按四步路径读它——它的核心 API 有几个函数?encode 和 decode 的签名告诉了你什么往返性质?

4. 对照思考:cabal 文件没有「仓库地址、镜像、插件、任务」这些 Gradle 常客——它们去哪了?这种「构建描述只声明什么、不编排怎么做」的取舍,你的直觉里是优点还是限制?

5. 给第 1 题的项目补 CI:抄本章的 Actions 骨架,跑通一次绿色流水线,确认缓存生效(对比两次运行的构建耗时)。