跳轉至內容

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: johndoe@example.com]
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:          dons@cse.unsw.edu.au
   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? - "<dons@cse.unsw.edu.au>" [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? - "<dons@cse.unsw.edu.au>" [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 中的精心設計的測試套件,這樣使用者就不會被它打擾。 它還可以揭示我們程式碼中的隱藏假設和遺漏——也許你的程式碼之所以能夠構建和執行,是因為一個意外生成的檔案。
華夏公益教科書