跳转至内容

Haskell/打包

来自维基教科书,开放世界中的开放书籍

创建新的 Haskell 项目或程序的最佳实践指南。

[编辑 | 编辑源代码]

几乎所有新的 Haskell 项目都使用以下工具。每个工具本身都很有用,但使用一组通用的工具也有助于提高每个人的生产力,而且您更有可能获得补丁。

构建系统

[编辑 | 编辑源代码]

使用 Cabal。您应该阅读 Cabal 用户指南。特别是,第二部分 以及 第三部分 将非常有帮助。为了生成项目,Cabal 依赖于 Git,因此如果您还没有安装 Git,我们建议您安装它。

对于库,使用 Haddock

纯代码可以使用 QuickCheck(对于 tasty 集成,推荐使用 tasty-quickcheck),hedgehogSmallCheck(尽管截至 2023 年,他们建议使用 falsify 以及 tasty),而杂代码可以使用 tasty-hunit 进行测试。

要入门,请尝试 Haskell/测试。对于稍微高级一点的介绍,Haskell 中的简单单元测试 是一篇关于使用一些模板 Haskell 为 QuickCheck 创建测试框架的博客文章。

简单项目的结构

[编辑 | 编辑源代码]

您可以使用 `cabal init` 生成新的 Haskell 项目。默认情况下,它会以交互方式询问您一些问题,以帮助您设置项目。对于大多数问题,默认值都可以。

但是,如果您禁用交互模式,则它不会询问您问题。假设 `$DIR` 是当前目录的名称。它将生成一个具有以下形状的项目

  • app/Main.hs -- 主要 Haskell 源代码文件
  • $DIR.cabal -- Cabal 构建描述
  • CHANGELOG.md

您当然可以修改它,添加子目录和多个模块。

以下是一段使用 Git 和 Cabal 构建、安装和发布的最小 Haskell 项目的创建过程。

命令工具 'cabal init' 会自动完成所有这些操作,但首先了解所有部分很重要。

我们现在将逐步介绍简单 Haskell 可执行文件基础设施的创建过程。库的相关建议将在后面给出。

创建目录

[编辑 | 编辑源代码]

为源代码创建目录

$ mkdir haq
$ cd haq

使用 Cabal 生成项目骨架

[编辑 | 编辑源代码]

首先,运行 `cabal update` 来更新 Cabal 的存储库。

如前所述,运行 `cabal init` 并根据您的需要回答问题。

What does the package build:
   1) Library
 * 2) Executable
   3) Library and Executable
   4) Test suite
Your choice? [default: Executable] 2
Do you wish to overwrite existing files (backups will be created) (y/n)? [default: n] y
Please choose version of the Cabal specification to use:
   1) 1.24  (legacy)
   2) 2.0   (+ support for Backpack, internal sub-libs, '^>=' operator)
   3) 2.2   (+ support for 'common', 'elif', redundant commas, SPDX)
   4) 2.4   (+ support for '**' globbing)
 * 5) 3.0   (+ set notation for ==, common stanzas in ifs, more redundant commas, better pkgconfig-depends)
   6) 3.4   (+ sublibraries in 'mixins', optional 'default-language')
Your choice? [default: 3.0] 5
Package name? [default: haq]
Package version? [default: 0.1.0.0]
Please choose a license:
   1) BSD-2-Clause
   2) BSD-3-Clause
   3) Apache-2.0
   4) MIT
   5) MPL-2.0
   6) ISC
   7) GPL-2.0-only
   8) GPL-3.0-only
   9) LGPL-2.1-only
  10) LGPL-3.0-only
  11) AGPL-3.0-only
  12) GPL-2.0-or-later
  13) GPL-3.0-or-later
  14) LGPL-2.1-or-later
  15) LGPL-3.0-or-later
  16) AGPL-3.0-or-later
  17) Other (specify)
Your choice? 16
Author name? [default: John Doe]
Maintainer email? [default: [email protected]]
Project homepage URL? [optional]
Project synopsis? [optional] Haqify code
Project category:
   1) Codec
   2) Concurrency
   3) Control
   4) Data
   5) Database
   6) Development
   7) Distribution
   8) Game
   9) Graphics
  10) Language
  11) Math
  12) Network
  13) Sound
  14) System
  15) Testing
  16) Text
  17) Web
  18) Other (specify)
Your choice? [default: (none)]
What is the main module of the executable:
 * 1) Main.hs
   2) Main.lhs
   3) Other (specify)
Your choice? [default: Main.hs]
Application directory:
 * 1) app
   2) exe
   3) src-exe
   4) Other (specify)
Your choice? [default: app]
Choose a language for your executable:
 * 1) Haskell2010
   2) Haskell98
   3) GHC2021 (requires at least GHC 9.2)
   4) Other (specify)
Your choice? [default: Haskell2010] 3
Add informative comments to each field in the cabal file. (y/n)? [default: y]
[Log] Using cabal specification: 3.0
[Log] Creating fresh file LICENSE...
[Log] Creating fresh file CHANGELOG.md...
[Log] Creating fresh directory ./app...
[Log] Creating fresh file app/Main.hs...
[Log] Creating fresh file haq.cabal...
[Warning] No synopsis given. You should edit the .cabal file and add one.
[Info] You may want to edit the .cabal file and add a Description field.

对于其中大多数问题,您只需选择自己的答案,但正如我们之前指出的,默认值是可以的。对于选择语言的问题,您应尽可能选择最新存在的语言。

如果您的包使用其他包,例如array,则需要将它们添加到Build-DependsCabal 文件中的字段。


编写一些 Haskell 代码

[编辑 | 编辑源代码]

编写您的程序

$ cat > app/Main.hs
--
-- Copyright (c) 2006 Don Stewart - http://www.cse.unsw.edu.au/~dons
-- GPL version 2 or later (see http://www.gnu.org/copyleft/gpl.html)
--
import System.Environment

-- 'main' runs the main program
main :: IO ()
main = getArgs >>= print . haqify . head

haqify s = "Haq! " ++ s

放入 Git 中

[编辑 | 编辑源代码]
$ git init
$ git add --all
$ git commit -m 'Import haq source'
$ ls -A
.git app CHANGELOG.md LICENSE haq.cabal

构建您的项目

[编辑 | 编辑源代码]

现在构建它!

$ cabal build

运行它

[编辑 | 编辑源代码]

现在您可以运行您的酷炫项目了

$ cabal run exe:haq -- me
"Haq! me"

构建一些 Haddock 文档

[编辑 | 编辑源代码]

将一些 API 文档生成到 dist/doc/* 中

$ cabal haddock --haddock-all

它将告诉您文档的生成位置。您可以通过以下方式查看它

$ w3m -dump dist-newstyle/build/$ARCH/ghc-x.x.x/haq-x.x.x.x/doc/html/package/Main.html # this is only to show what the path will vaguely look like
 haq Contents Index
 Main

 Synopsis
 main :: IO ()

 Documentation

 main :: IO ()
 main runs the main program

 Produced by Haddock version 0.7

没有输出?确保您已经安装了 Haddock。

添加一些自动化测试:QuickCheck

[编辑 | 编辑源代码]

我们将使用 QuickCheck 来指定 Haq.hs 代码的简单属性。创建一个测试模块 Tests.hs,其中包含一些 QuickCheck 样板代码

$ cat > Tests.hs
import Char
import List
import Test.QuickCheck
import Text.Printf

main  = mapM_ (\(s,a) -> printf "%-25s: " s >> a) tests

instance Arbitrary Char where
    arbitrary     = choose ('\0', '\128')
    coarbitrary c = variant (ord c `rem` 4)

现在让我们编写一个简单的属性

$ cat >> Tests.hs 
-- reversing twice a finite list, is the same as identity
prop_reversereverse s = (reverse . reverse) s == id s
    where _ = s :: [Int]

-- and add this to the tests list
tests  = [("reverse.reverse/id", test prop_reversereverse)]

现在我们可以运行此测试,并让 QuickCheck 生成测试数据

$ runhaskell Tests.hs
reverse.reverse/id       : OK, passed 100 tests.

让我们为 'haqify' 函数添加一个测试

-- Dropping the "Haq! " string is the same as identity
prop_haq s = drop (length "Haq! ") (haqify s) == id s
    where haqify s = "Haq! " ++ s

tests  = [("reverse.reverse/id", test prop_reversereverse)
        ,("drop.haq/id",        test prop_haq)]

并且让我们测试

$ runhaskell Tests.hs
reverse.reverse/id       : OK, passed 100 tests.
drop.haq/id              : OK, passed 100 tests.

太棒了!

从 Darcs 运行测试套件

[编辑 | 编辑源代码]

我们可以安排 darcs 在每次提交时运行测试套件。

$ darcs setpref test "runhaskell Tests.hs"
Changing value of test from '' to 'runhaskell Tests.hs'

将运行完整的 QuickChecks 集。(如果您的测试需要,您可能需要确保其他内容也已构建,例如darcs setpref test "alex Tokens.x;happy Grammar.y;runhaskell Tests.hs").

让我们提交一个新的补丁。

$ darcs add Tests.hs
$ darcs record --all
What is the patch name? Add testsuite
Do you want to add a long comment? [yn]n
Running test...
reverse.reverse/id       : OK, passed 100 tests.
drop.haq/id              : OK, passed 100 tests.
Test ran successfully.
Looks like a good patch.
Finished recording patch 'Add testsuite'

太好了,现在补丁必须通过测试套件才能被提交。

标记稳定版本,创建 tarball,然后出售它!

[编辑 | 编辑源代码]

标记稳定版本

$ darcs tag
What is the version name? 0.0
Finished tagging patch 'TAG 0.0'

高级 Darcs 功能:延迟获取

[编辑 | 编辑源代码]

随着您的存储库积累补丁,新用户可能会对完成初始操作所需的时间感到厌烦darcs get。(一些项目,如 yi 或 GHC,可能包含数千个补丁。)Darcs 速度足够快,但下载数千个单独的补丁仍然需要一段时间。有没有什么方法可以提高效率?

Darcs 提供了--lazy选项以darcs get。这使得能够只下载存储库的最新版本。如果需要,补丁将在以后按需下载。


在分发您的 Haskell 程序时,您大致有三个选择

  1. 通过 Darcs 存储库分发
  2. 分发 tarball
    1. 一个 Darcs tarball
    2. 一个 Cabal tarball

对于 Darcs 存储库,如果它是公开的,那么您就完成了。但是:也许您没有使用 Darcs 的服务器,或者也许您的计算机没有为人们设置darcs pull从中获取。在这种情况下,您需要通过 tarball 分发源代码。

通过 darcs 获取 tarball
[编辑 | 编辑源代码]

Darcs 提供了一个命令,它将制作一个压缩的 tarball,并将它管理的所有文件副本放入其中。(注意,_darcs 中的任何内容都不会被包含在内 - 它只包含您的源文件,没有修订历史记录。)

$ darcs dist -d haq-0.0
Created dist as haq-0.0.tar.gz

一切都准备好了!

通过 Cabal 获取 tarball
[编辑 | 编辑源代码]

由于我们的代码是 cabalised 的,我们可以直接使用 Cabal 创建 tarball

$ runhaskell Setup.lhs sdist
Building source dist for haq-0.0...
Source tarball created: dist/haq-0.0.tar.gz

与 Darcs 生成的 tarball 相比,这有优点和缺点。主要优点是 Cabal 将对我们的存储库进行更多检查,更重要的是,它将确保 tarball 具有 HackageDB 和 cabal-install 所需的结构。

但是,它确实有一个缺点:它只打包构建项目所需的那些文件。它会故意不包含存储库中的其他文件,即使它们在某个时候被证明是必要的[1]。要包含其他文件(例如Test.hs在上例中),我们需要在 cabal 文件中添加类似的代码行

extra-source-files: Tests.hs

如果我们有它们,我们可以确保 AUTHORS 或 README 等文件也被包含在内

data-files: AUTHORS, README

创建了以下文件

   $ ls
   Haq.hs           Tests.hs         dist             haq.cabal
   Setup.lhs        _darcs           haq-0.0.tar.gz

创建 Haskell 库的过程几乎相同。对于假设的“ltree”库,区别如下

层次结构源

[编辑 | 编辑源代码]

源代码应该位于适合现有 模块布局指南 的目录路径下。因此,对于模块 Data.LTree,我们将创建以下目录结构

   $ mkdir Data
   $ cat > Data/LTree.hs 
   module Data.LTree where

因此,我们的 Data.LTree 模块位于 Data/LTree.hs 中

Cabal 文件

[编辑 | 编辑源代码]

库的 Cabal 文件列出了公开可见的模块,并且没有可执行部分

   $ cat ltree.cabal 
   Name:                ltree
   Version:             0.1
   Description:         Lambda tree implementation
   License:             BSD3
   License-file:        LICENSE
   Author:              Don Stewart
   Maintainer:          [email protected]
   Build-Depends:       base
   Exposed-modules:     Data.LTree

因此,我们可以构建我们的库

   $ runhaskell Setup.lhs configure --prefix=$HOME --user
   $ runhaskell Setup.lhs build    
   Preprocessing library ltree-0.1...
   Building ltree-0.1...
   [1 of 1] Compiling Data.LTree       ( Data/LTree.hs, dist/build/Data/LTree.o )
   /usr/bin/ar: creating dist/build/libHSltree-0.1.a

我们的库已创建为对象存档。在 *nix 系统上,您可能需要在配置步骤中添加 --user 标志(这意味着您希望在安装过程中更新本地软件包数据库)。现在安装它

   $ runhaskell Setup.lhs install
   Installing: /home/dons/lib/ltree-0.1/ghc-6.6 & /home/dons/bin ltree-0.1...
   Registering ltree-0.1...
   Reading package info from ".installed-pkg-config" ... done.
   Saving old package config file... done.
   Writing new package config file... done.

我们完成了!您可以从例如 ghci 中使用您的新库

   $ ghci -package ltree
   Prelude> :m + Data.LTree
   Prelude Data.LTree> 

新库已在范围内,随时可以使用。

更复杂的构建系统

[编辑 | 编辑源代码]

对于大型项目,将源代码树存储在子目录中很有用。这可以通过简单地创建一个目录来完成,例如,“src”,您将在其中放置您的 src 树。

要让 Cabal 找到此代码,您需要在 Cabal 文件中添加以下代码行

   hs-source-dirs: src

Cabal 可以设置成除了运行配置脚本之外,还可以运行一系列其他功能。有关更多信息,请参阅 Cabal 文档

内部模块

[编辑 | 编辑源代码]

如果您的库使用未公开的内部模块,请不要忘记在other-modules字段中列出它们

   other-modules: My.Own.Module

如果未这样做(截至 GHC 6.8.3),可能会导致您的库在没有错误的情况下构建,但实际上无法从应用程序中使用,这将在构建时出现链接器错误。

自动化

[编辑 | 编辑源代码]

cabal init

[编辑 | 编辑源代码]

一个名为 cabal-install 的 Haskell 包管理工具提供了一个命令行工具来帮助开发人员创建一个简单的 cabal 项目。只需运行并回答所有问题。每个问题都提供了默认值。

$ cabal init
Package name [default "test"]? 
Package version [default "0.1"]? 
Please choose a license:
...

mkcabal 是一个在 cabal init 之前存在的工具,它也会自动填充一个新的 cabal 项目 

   darcs get http://code.haskell.org/~dons/code/mkcabal

注意:此工具在 Windows 中不起作用。Windows 版本的 GHC 不包含此工具所需的 readline 软件包。

使用方法是

$ mkcabal
Project name: haq
What license ["GPL","LGPL","BSD3","BSD4","PublicDomain","AllRightsReserved"] ["BSD3"]: 
What kind of project [Executable,Library] [Executable]: 
Is this your name? - "Don Stewart " [Y/n]: 
Is this your email address? - "<[email protected]>" [Y/n]: 
Created Setup.lhs and haq.cabal
$ ls
Haq.hs    LICENSE   Setup.lhs _darcs    dist      haq.cabal

这将为项目“haq”填写一些存根 Cabal 文件。

要创建一个全新的项目树

$ mkcabal --init-project
Project name: haq
What license ["GPL","LGPL","BSD3","BSD4","PublicDomain","AllRightsReserved"] ["BSD3"]: 
What kind of project [Executable,Library] [Executable]: 
Is this your name? - "Don Stewart " [Y/n]: 
Is this your email address? - "<[email protected]>" [Y/n]: 
Created new project directory: haq
$ cd haq
$ ls
Haq.hs    LICENSE   README    Setup.lhs haq.cabal

许可证

[编辑 | 编辑源代码]

用于公共基础库包的代码必须是 BSD 许可的或更免费/开放的。否则,完全取决于您作为作者。

选择一个许可证(灵感来自 this)。检查您使用的东西的许可证,包括其他 Haskell 软件包和 C 库,因为这些可能强加您必须遵守的条件。

尽可能使用与相关项目相同的许可证。Haskell 社区大致分为两派,一派将所有内容发布在 BSD 或公共领域下,另一派是 GPL/LGPLers(这种划分大致反映了自由软件社区中的复制许可/非复制许可划分)。一些 Haskellers 特别建议避免使用 LGPL,因为存在跨模块优化问题。与许多许可问题一样,这条建议是有争议的。一些 Haskell 项目(wxHaskell、HaXml 等)使用 LGPL,并附加了一个更宽松的条款,以避免跨模块优化问题。

将您的代码发布为稳定的、带标签的 tarball 很重要。不要只 依靠 darcs 进行分发

  • darcs dist 直接从 darcs 存储库生成 tarball

例如

$ cd fps
$ ls       
Data      LICENSE   README    Setup.hs  TODO      _darcs    cbits dist      fps.cabal tests
$ darcs dist -d fps-0.8
Created dist as fps-0.8.tar.gz

您现在只需发布您的 fps-0.8.tar.gz

您还可以通过使用后挂钩让 darcs 为您完成等同于“每日快照”的操作。

将以下内容放入 _darcs/prefs/defaults 中

 apply posthook darcs dist
 apply run-posthook

建议

  • 使用 darcs tag 标记每个版本。例如
$ darcs tag 0.8
Finished tagging patch 'TAG 0.8'

然后人们就可以darcs get --lazy --tag 0.8,以获取仅标记的版本(而不是整个历史记录)。

您可以在 http://patch-tag.com/ 免费托管公共和私有 Darcs 仓库。 否则,只需将 Darcs 仓库从网页上提供即可发布。 另一种选择是在 Haskell 社区服务器 http://code.haskell.org/ 上托管。 您可以通过 http://community.haskell.org/admin/ 申请帐户。 您也可以使用 https://github.com/ 用于 Git 托管。

一个 完整的示例 说明了在此过程中编写、打包和发布新的 Haskell 库。



注意

  1. 这实际上是一件好事,因为它允许我们做一些事情,比如创建一个不会包含在 tarball 中的精心设计的测试套件,这样用户就不会被它打扰。 它还可以揭示我们代码中的隐藏假设和遗漏——也许你的代码之所以能够构建和运行,是因为一个意外生成的文件。
华夏公益教科书