Haskell入门安装实战:从GHCup配置到cabal项目运行
2026/9/13 3:46:50 网站建设 项目流程

Haskell 入门学习(一)之安装试用 Haskell

哪个程序员没有听过 Haskell 的大名?纯函数式、惰性求值、强静态类型、类型推导……这些标签随便拎一个出来,都够在技术讨论里撑足场面。但很多朋友卡在第一步:到底怎么在自己的电脑上把 Haskell 跑起来?网上教程要么太老,要么直接扔给你一串命令然后默认你懂。这篇文章没有任何前置知识要求,我会带你从零开始,把安装、配置、写第一行代码、跑起第一个项目的完整流程走一遍。目标是让你在今天下班前,就能在自己电脑上写 Haskell,并且知道每一行命令到底在干什么。

我先说结论:2024 年装 Haskell,你只需要记住一个名字——GHCup。它是 Haskell 官方的工具链管理器,类似 Python 界的 conda、Node 界的 nvm。剩下的 GHC(编译器)、cabal(构建工具)、stack(另一个构建工具)、HLS(语言服务器),全都交给 GHCup 统一安装和切换。你不需要提前理解这些名词,跟着做就行,每步我都会解释它是干嘛的。

1. 写在安装前:理清 Haskell 生态里的几个“第一次见面”的名字

很多初学者在安装阶段就放弃,不是因为命令有多难,而是因为被一堆缩写砸懵了。GHC、GHCi、cabal、stack、HLS、Cabal……这些名词之间的关系,我用人话给你捋一遍。

GHC(Glasgow Haskell Compiler)是 Haskell 最主流的编译器,你可以把它理解成“能把 Haskell 代码变成机器能跑的可执行文件的那把刀”。绝大多数时候,我们说“装 Haskell”,实际就是在装 GHC。GHCi则是 GHC 自带的交互式环境,你在终端里敲ghci进入的那个提示符,有点类似 Python 的>>>交互界面,适合随手验证小函数和表达式。

cabalstack都是构建工具,负责帮你管理项目依赖、编译顺序、打包发布。打个比方,GHC 是厨师的炒锅,cabal 和 stack 就是帮厨,帮你把需要的食材(依赖库)提前按清单买好洗好切好,再递给厨师下锅。现阶段你不需要在两者之间做艰难抉择,GHCup 默认会把 cabal 一起装好,我们先用 cabal,它更贴近官方主流;stack 以后用到再装也不迟。

HLS(Haskell Language Server)是给编辑器做代码补全、类型提示、跳转定义的“幕后大脑”。你用 VS Code 写 Haskell 时的那些智能提示,全靠它在后台干活。GHCup则是总管家,负责把上面这些工具装好、升级、切换版本。明白这个关系后,安装思路就非常清晰了:先装 GHCup,再用 GHCup 装 GHC、cabal、HLS

还有一个概念叫LTS(Long Term Support),是 stack 生态里的一套经过互相兼容测试的依赖版本集合。你暂时不用深究,只需要知道它存在的意义是“帮你避坑”,让你不用自己纠结哪个库和哪个编译器版本能和平共处。

2. 安装实战:GHCup 全流程图文级拆解

先说明一点,Haskell 工具链是跨平台的,Windows、macOS、Linux 都能跑。我在三套环境里都装过,踩过一些坑,下面按平台把流程走一遍。无论你用哪个系统,核心逻辑都是同一个:先装 GHCup,再通过 GHCup 装 GHC、cabal 和 HLS

2.1 Linux / macOS 环境安装:一条命令搞定 90% 工作

在终端里执行下面这条命令:

curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh

看到这条命令不要慌,它的意思是用 curl 下载 GHCup 的安装脚本,然后交给 sh 执行。整个安装过程会问你几个问题,我逐个说怎么选:

  • “Do you want GHCup to automatically add the required PATH entries to /home/xxx/.bashrc?”:输入y,让安装脚本自动把 GHCup 的可执行文件路径加进环境变量。如果你不想自动改配置文件,也可以输入n手动配置,但新手不建议给自己找麻烦,直接y
  • “Do you want to install the Haskell Language Server (HLS)?”:输入y,HLS 是后续用编辑器写代码的体验保障,现在装好省得日后补。
  • “Do you want to install stack?”:这一步看心情,建议输入y,stack 在某些项目里还是刚需,一次装齐省心。

装完后,关掉当前终端重新打开(让 PATH 生效),或者手动执行一下source ~/.bashrc(macOS 用户可能是source ~/.zshrc)。然后验证安装结果:

ghcup --version

如果能看到版本号输出,说明 GHCup 安装成功。接着用 GHCup 安装 GHC 编译器:

ghcup install ghc recommended ghcup set ghc recommended

第一条命令安装“推荐版本”的 GHC,第二条命令把它设为默认版本。你可能好奇为什么不用最新版?这里有个经验之谈:Haskell 的第三方库生态更新往往滞后于编译器版本,选 recommended 而不是 latest,能让你少踩很多“库还不兼容新编译器”的坑

安装完成后,验证 GHC 和 cabal:

ghc --version cabal --version git --version

第三条命令里的 git 不是 Haskell 工具链自带的,但 cabal 在拉取远程依赖包时需要用到 git,如果系统里没有,顺手装上(macOS 用brew install git,Ubuntu 用apt install git)。这三条命令都正常输出版本号,Linux/macOS 端的安装就完事了。

2.2 Windows 环境安装:原生安装与 WSL 二选一

Windows 用户有两条路可走,我用亲身体验分别说下。

路线一:原生安装(适合不想折腾虚拟机的朋友)

先去 https://www.haskell.org/ghcup/ 下载最新的ghcup-x86_64-mingw64.exe安装包,双击运行。这个图形化安装器会引导你勾选要装的组件,默认勾选已经包含 GHC、cabal、HLS、stack,直接点“Install”即可。装完后需要重启终端(或者重启电脑),确保环境变量生效。

路线二:WSL 2(适合后续想深度折腾 Linux 生态的朋友)

如果你电脑装的是 Windows 10/11,我强烈推荐用 WSL 2(Windows Subsystem for Linux 2)。简单说,WSL 2 让你在 Windows 里跑一个真正的 Linux 环境,而 Haskell 相关的许多工具和教程默认优先支持 Linux/macOS。在 WSL 的终端里,按上面 Linux 的流程走一遍即可。

如果按上面流程操作,遇到“找不到 ghcup 命令”的情况,需要手动把 GHCup 的路径加进 PATH。在 PowerShell 里执行:

$env:Path += ";$env:USERPROFILE\AppData\Roaming\ghcup\bin"

这只是临时生效,永久生效需要去“系统属性 -> 环境变量”里把%USERPROFILE%\AppData\Roaming\ghcup\bin手动加到 Path 变量中。相比之下,WSL 2 的安装流程比 Windows 原生更顺滑,这也是我更推荐后者的原因。

2.3 安装完必做的三件事:更新包列表、测试编个“Hello”、理清工具链

环境变量和编译器都就绪后,先别急着写代码,花两分钟做三件事,能帮你避开后期不少疑难杂症。

第一件事:更新 cabal 的包列表

cabal update

这条命令会从 Hackage(Haskell 的官方包仓库,类似 Python 的 PyPI)拉取最新的软件包索引。第一次执行会比较慢,因为要下载的索引文件包含几十万个包的信息。如果网络状况不佳,可能会中途失败,多试几次或者换网络环境即可。这个索引是 cabal 搜索和安装依赖的基础,建议每次新环境装完后都执行一遍。

第二件事:写一个最简单的 Haskell 文件,命令行直接编译运行

echo 'main = putStrLn "Hello, Haskell!"' > hello.hs ghc -o hello hello.hs ./hello

这里逐条解释一下:echo把一行 Haskell 代码写入hello.hs文件;ghc -o hello hello.hs是让 GHC 编译器把源码编译成名为hello的可执行文件;./hello是运行它。如果你看到终端输出了 “Hello, Haskell!”,恭喜你,工具链已经完整跑通了。编译过程中如果一个.hi文件和.o文件生成在旁边,那是正常的中间产物,不用管。

第三件事:全局配置 git 用户信息

如果你打算长期使用 Haskell(特别是后面要用 cabal 从远程仓库拉代码),建议提前配置 git 身份信息:

git config --global user.name "你的名字" git config --global user.email "你的邮箱"

这一步不是 Haskell 独有的,但很多基于 cabal/stack 的项目在拉取/发布包时会用到 git 身份,不配置经常会遇到各种奇怪的报错。

3. 初次见面 GHCi:把 Haskell 当计算器玩一遍

工具链就绪后,我们现在进入“试用”环节。GHCi 是 Haskell 的交互式解释器,是新手体验这门语言特性的最佳场所——写完一行立刻就能看到结果,不用经历“编译-运行”的完整周期。

在终端输入:

ghci

看到类似下面的提示符就说明进入交互环境了:

GHCi, version 9.4.7: https://www.haskell.org/ghc/ :? for help ghci>

3.1 表达式与类型:入门的第一性原理

GHCi 可以像计算器一样算数学表达式:

ghci> 1 + 2 3 ghci> 3 * 4 12 ghci> 2 ^ 10 1024

注意^在 Haskell 里表示乘方(指数运算),这对来自 Python/C 背景的朋友来说要特别留意,后面写算法时很容易在这里踩坑。

Haskell 最迷人的地方之一是静态类型系统,但它又聪明到不需要你写类型标注,GHCi 会通过类型推导自动帮你算出来。你可以用:t命令查看任意表达式的类型:

ghci> :t 42 42 :: Num a => a ghci> :t "Hello" "Hello" :: [Char] ghci> :t True True :: Bool

42 :: Num a => a的意思是:42 可以适配任意数值类型(Int、Integer、Double 等都行),这就是 Haskell 的“多态”概念。现在还不需要完全理解,你先熟悉一下:::前面是值,后面是类型。以后看文档、读报错信息,都会频繁见到这种写法。

3.2 定义第一个函数:从交互环境中体验函数式思维

在 GHCi 里,用let可以定义函数(是的,GHCi 里需要let,但在写.hs源文件时不需要):

ghci> let double x = x * 2 ghci> double 5 10 ghci> double (double 5) 20

这里有几个要时刻记住的细节:

  • 函数调用不是用括号,而是用空格。在 Python 里是double(5),在 Haskell 里是double 5。如果参数本身是个表达式,才需要加括号保平安,比如double (double 5)
  • 函数定义把参数写在函数名后面,用等号连接函数体let double x = x * 2的意思是:定义一个名为 double 的函数,它接受一个参数 x,返回 x * 2。
  • Haskell 里没有“赋值语句”这个概念,等号是“定义”,一个名字一旦绑定就不会被重新赋值。这和命令式语言里的“变量”有本质区别,这种特性在并发编程里有巨大优势,以后你会发现它让代码推理轻松很多。

再看一个多参数函数:

ghci> let add x y = x + y ghci> add 3 4 7 ghci> :t add add :: Num a => a -> a -> a

add的类型签名a -> a -> a读作:接收一个类型为a的参数,再接收一个类型为a的参数,最后返回一个类型为a的值。箭头->是函数类型的标记,Haskell 里一切皆函数,连运算符本质上都是函数

3.3 懒加载初体验:试着“偷个小懒”

Haskell 最突出的特征之一是惰性求值,意思是:表达式不会立即计算,只在真正需要值时才会求值。看这个例子:

ghci> let infiniteList = [1..] ghci> take 5 infiniteList [1,2,3,4,5]

[1..]在 Python 里等价于用一个无限 range,直接跑会撑爆内存,但 Haskell 里定义一个无限列表毫无压力,因为take 5只需要前 5 个元素时,后面的元素就还没被“算出来”。这种特性让 Haskell 表达无限数据结构变得非常自然,也会一步步颠覆你对“程序执行顺序”的直觉。现在体会一下这种感觉就行:表达式是描述“是什么”,而不是一步步“怎么做”

:quit或按 Ctrl+D 能退出 GHCi,回到系统终端。

4. 从交互到“工程化”:用 cabal 初始化你的第一个 Haskell 项目

玩了一阵子 GHCi 后,你会发现纯靠交互环境没法组织大代码——代码文件、模块依赖、测试、打包,都需要工程化工具来管理。这就是 cabal 登场的时候。cabal 是 Haskell 官方的构建系统,功能对齐 Python 的 poetry 或 Node 的 npm

4.1 项目初始化命令逐行解读

先创建一个项目目录并进入:

mkdir hello-project cd hello-project

在当前目录下执行:

cabal init

cabal 会交互式问你几个问题,我给出推荐选项并解释为什么要这么选:

  • “What does the package do?”:随便填,比如A simple Haskell program。这段描述最终会写进.cabal文件,只影响元信息,不影响编译。
  • “What license?”:选MITBSD-2-Clause这类宽松许可证,后续开源省心。
  • “What is the main module called?”:默认就是Main,直接回车。
  • “Select the language for your project” : 选GHC2021(或Haskell2010),前者是 2021 年的语言规范,属于现代 Haskell 的默认配置;实际上 GHCup 推荐的 GHC 版本已经默认支持 GHC2021,直接回车即可。

cabal init执行完后,目录里会生成几个关键文件:

  • hello-project.cabal:整个项目的“说明书”,包含包名、版本、依赖库、入口模块等元信息。
  • app/Main.hs:主程序源文件,默认包含一个打印 “Hello, Haskell!” 的 main 函数。
  • CHANGELOG.md:记录版本变更的日志文件。
  • cabal.project:项目级 cabal 配置,新版本 GHCup 默认生成的 cabal.project 一般不需要改动。

这才是“一个规范 Haskell 项目”的最小骨架——不是我刚才敲单文件那种野路子,而是能持续扩展、打包分发、被其他人维护的标准化结构。

4.2 构建、运行、清理:cabal 入门三板斧

在项目目录下依次执行:

cabal build cabal run

cabal build会编译整个项目及其依赖。第一次执行会有大量日志输出,因为 cabal 要先把依赖包(比如 base 库)也拉下来编译一遍,耗时几秒到几分钟不等,取决于网络和机器性能。看到输出末尾出现Build completed或类似字样,就说明编译通过了。

cabal run会运行项目的入口函数。初始化模板生成的app/Main.hs内容是:

module Main (main) where main :: IO () main = putStrLn "Hello, Haskell!"

如果你看到了 “Hello, Haskell!”,恭喜,你已经完成了一个规范 Haskell 项目的完整生命周期。想清理编译产物,执行cabal clean;想看当前项目的依赖树,执行cabal list --installed(列出全局已安装包)或cabal info <包名>(查看某个包的信息)。

4.3 用 GHCi 加载自己的源文件:随时调试项目代码

很多初学者纠结:cabal 项目里的代码,怎么快速验证某个函数?其实可以在项目根目录直接运行:

cabal repl

cabal repl会启动一个 GHCi,但自动加载了当前项目的所有模块和依赖。比如我改一下app/Main.hs,在里面加一个新函数:

module Main (main) where double :: Int -> Int double x = x * 2 main :: IO () main = print (double 21)

保存后在项目根目录执行cabal repl,进入交互界面后:

ghci> double 10 20 ghci> double (double 10) 40 ghci> :r -- 重新加载代码文件

在 GHCi 里按:r会重新编译并加载修改过的源文件,这比每次改代码都要退出重进方便得多。如果你只是简单试验,也可以用ghci app/Main.hs直接加载指定文件,但它们之间的区别在于:cabal repl能正确解析项目其他模块的依赖关系,ghci <文件名>则只能处理单个文件。

5. 实战:写一个稍微像样的程序——猜数字小游戏

前面吃的都是“前菜”,现在我们把安装、解释器、项目构建、函数定义串起来,写一个完整的、和用户真实交互的小程序。猜数字游戏是各语言入门经典,用 Haskell 写一遍能让你体会它和 Python/JavaScript 的思维差异到底在哪。

目标逻辑:程序随机生成一个 1 到 100 的整数,用户在终端循环输入猜数,程序提示“高了”“低了”或“猜对了”。

创建一个新文件guess.hs(用cabal init初始化后的项目里,可以直接写在app/Main.hs里,然后cabal run),内容如下:

module Main (main) where import System.Random import Text.Read (readMaybe) play :: Int -> IO () play target = do putStrLn "猜一个 1 到 100 之间的数字:" input <- getLine case readMaybe input :: Maybe Int of Nothing -> do putStrLn "请输入合法的整数!" play target Just guess | guess < target -> do putStrLn "太低了,再试一次" play target | guess > target -> do putStrLn "太高了,再试一次" play target | otherwise -> putStrLn "恭喜你,猜对了!" main :: IO () main = do gen <- getStdGen let (target, _) = randomR (1, 100) gen putStrLn "我已经想好了一个 1 到 100 之间的数字,开始猜吧!" play target

5.1 逐段拆解:这里面藏着 Haskell 的几个“第一次”

这段代码里有很多初学者容易卡住的点,我逐行拆开讲明白。

从最简单的main开始:

main = do gen <- getStdGen let (target, _) = randomR (1, 100) gen putStrLn "……" play target

do关键字标记一个“命令式代码块”,里面按顺序执行多条 IO 语句。getStdGen从系统获取一个随机数生成器,randomR (1, 100) gen生成一个指定范围内的随机数,返回(target, newGen)这样的二元组,我们只关心第一个值,所以用_忽略第二个。Haskell 里变量一旦绑定就不会改变,但我们可以通过返回“新状态”来延续状态,这是函数式编程中处理状态的经典模式。

再看play函数:

play target = do putStrLn "猜一个 1 到 100 之间的数字:" input <- getLine ...

getLine读取用户输入的一行字符串(结果带换行符),<-把它绑定到名字input。注意input的类型是String,想和target(整数)比较大小,必须把String转成整数。这里我用了readMaybe

case readMaybe input :: Maybe Int of

readMaybe的返回值类型是Maybe IntMaybe是 Haskell 处理“可能失败”的值的方式——它要么是Just 42,表示成功拿到一个整数值,要么是Nothing,表示解析失败。如果你是从 Java 过来的,Maybe基本等同于Optional;从 Python 过来,可以理解为返回None或返回具体值。用case ... of把两种情况都处理到,编译器会强制你考虑所有分支,这也就避免了大量“空指针”问题——语言层面就帮你消灭了一整类 bug。

判断猜大猜小用了 Haskell 的“守卫”语法:

Just guess | guess < target = ... | guess > target = ... | otherwise = ...

|后面是条件,从上往下依次匹配第一个条件为真的分支。这种写法比if-else嵌套更清晰,你会在很多 Haskell 代码里看到它。这里otherwise其实就是True的一个别名,是我们约定俗成的最后一根“兜底稻草”。

5.2 编译运行与程序行为

用 cabal 构建这个项目:

cd hello-project # 或你放置 guess.hs 的目录 cabal run

程序启动后,会提示你猜数字,我本地跑了一次,示例输出如下:

我已经想好了一个 1 到 100 之间的数字,开始猜吧! 猜一个 1 到 100 之间的数字: 50 太低了,再试一次 猜一个 1 到 100 之间的数字: 75 太高了,再试一次 猜一个 1 到 100 之间的数字: 62 恭喜你,猜对了!

一个完整的、包含随机数生成、字符串解析、递归循环、模式匹配、多分支判断的程序,就正常运行起来了。注意play里“输了继续玩”靠的是递归调用自身,而不是循环。这是函数式编程的常见形态:用递归表达循环,把状态作为参数传递下去。思维方式确实和命令式语言差别很大,但一旦习惯之后,你会发现写起来也挺顺。

6. 安装与试跑过程中最容易踩的 6 个坑

我把这段时间在各平台遇到的高频问题整理成一个速查表,你按顺序排查基本能解决 90% 的问题。

常见报错可能出现的原因解决办法
ghc: command not foundGHC 没安装或 PATH 没配置好运行ghcup install ghc recommended && ghcup set ghc recommended;关闭终端重开,或手动source ~/.bashrc
cabal: command not foundcabal 未安装或 PATH 未生效运行ghcup install cabal recommended;检查~/.ghcup/bin是否在 PATH 中
cabal update卡住或失败网络问题,或 Hackage 索引源较慢多试几次;换成国内镜像(阿里云、清华 tuna 都提供 Hackage 镜像);手动下载索引放到~/.cabal/packages
编译时报Could not find module ‘System.Random’Random 包依赖未安装在 cabal 项目的.cabal文件里,把random加到build-depends字段:build-depends: base ^>=4.x, random ^>=1.2,然后重新cabal build
Windows 原生安装后 ghcup 命令不存在环境变量未生效手动把%USERPROFILE%\AppData\Roaming\ghcup\bin加入系统 PATH,重启终端
VS Code 写代码无限转圈/无类型提示HLS 未安装或版本不匹配ghcup install hls recommended,然后在 VS Code 安装 Haskell 扩展并重启;确认状态栏显示 “HLS: connected”
ghc编译时报decode: 换行被忽略或报错定位到奇怪字符文件编码或换行符不统一确保.hs文件用 UTF-8 编码保存,在 Windows 上把 IDE 的换行符设置成 LF

这里有三个实操心得,希望能帮你少走弯路:

第一,第一次装完立刻做一次“冒烟测试”。我在 macOS 上曾经“成功”装完 GHC,但直到用ghc编译一个文件时才暴露 PATH 配置问题。所以装完别急着离开,立刻用ghc --version && cabal --version确认两边都好使,再进入下一步。

第二,多版本 GHC 切换不是玄学。你以后可能会同时维护多个项目,有的项目要求 GHC 8.10,有的要求 9.2。GHCup 支持同时装多个 GHC 版本,用ghcup set ghc 9.2.8就能全局切换,项目里也可以通过.ghc-version文件指定当前目录用哪个版本。这个机制不懂没关系,但你要留个印象:工具链版本是可控的,别怕。

第三,HLS 装不上也别气馁,可以先裸写。HLS 需要在本地构建一些辅助库,首次安装耗时较长,容易因为网络问题失败。如果你暂时不需要强大的代码补全,用系统自带的文本编辑器和 GHCi 也能正常学习——我用 GHCi 完成了前两周绝大部分的练习。

7. 学习路径建议与下一步展望

到这里,你已经完成了 Haskell 从安装到运行第一个项目的完整闭环,这其实是 Haskell 学习中最“物理”的一步——工具链通了,后面纯粹是思维模式的转换问题。

我的建议是:不要急着啃《Haskell 函数式编程入门》这类大部头,先用 GHCi 把基础类型(Int、Char、Bool、列表、元组)和常见函数(map、filter、foldr)玩熟练。每天花 20 分钟,用 Haskell 重写几个你用 Python 写过的简单函数,比如计算列表均值、反转字符串、判断素数、统计词频。这个过程会让你快速习惯“把逻辑拆成小函数再组合”的函数式风格。

等你对列表和数据结构的操作有感觉了,再系统学习自定义数据类型(data)、类型类(typeclass)、函子(Functor)和单子(Monad)这些抽象概念。这些概念单独拎出来确实抽象,但一旦你有了“写多个带 IO 的小程序”的经验,理解它们会比直接看书容易十倍。

我在实际落地中发现,最容易让人放弃 Haskell 的其实不是概念本身,而是切换语言的兴奋期过去后,遇到“为什么这段代码报错了”的那种挫败感。Haskell 的报错信息一开始确实抽象,遇到报错别慌,把报错信息粘贴到搜索引擎,绝大多数情况都能找到解释。等你熬过前两周的适应期,这种“挫败感”就会变成“原来如此”的成就感。

最后再分享一个实用技巧:Haskell 的社区氛围非常友好,StackOverflow 上关于 Haskell 的问题回复质量普遍很高,#haskell频道也欢迎新人提问。遇到卡住超过半小时的问题,先润色一下问题描述,发出去求助。很多时候,把问题想明白怎么写出来,答案自己就浮现了。祝你在 Haskell 的世界里玩得开心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询