Haskell LanguageHaskell語言入門


備註

Haskell標誌

Haskell是一種先進的純函數式編程語言。

特徵:

  • 靜態類型化: Haskell中的每個表達式都有一個在編譯時確定的類型。靜態類型檢查是基於對程序文本(源代碼)的分析來驗證程序的類型安全性的過程。如果程序通過靜態類型檢查程序,則程序保證滿足所有可能輸入的某些類型安全屬性。
  • 純粹的功能 :Haskell中的每個函數都是數學意義上的函數。沒有語句或指令,只有表達式不能改變變量(本地或全局),也不能訪問狀態,如時間或隨機數。
  • Concurrent:它的旗艦編譯器GHC帶有一個高性能並行垃圾收集器和輕量級並發庫,包含許多有用的並發原語和抽象。
  • 延遲評估:函數不評估其參數。延遲表達式的求值直到需要它的值。
  • 通用: Haskell可用於所有環境和環境。
  • 軟件包: Haskell的開源貢獻非常活躍,公共軟件包服務器上提供了大量軟件包。

Haskell的最新標準是Haskell 2010.截至2016年5月,一個小組正在研究下一個版本Haskell 2020。

Haskell官方文檔也是一個全面而有用的資源。尋找書籍,課程,教程,手冊,指南等的好地方。

版本

發布日期
Haskell 2010 2012-07-10
哈斯克爾98 2002-12-01

入門

在線REPL

開始編寫Haskell的最簡單方法可能是訪問Haskell網站嘗試Haskell並在主頁上使用在線REPL(read-eval-print-loop)。在線REPL支持大多數基本功能甚至一些IO。還有一個基本教程可以通過鍵入命令help 來啟動。一個理想的工具,開始學習Haskell的基礎知識並嘗試一些東西。

GHC(I)

對於準備參與更多的程序員來說, GHCiGlorious / Glasgow Haskell編譯器附帶的交互式環境。 GHC可以單獨安裝,但這只是一個編譯器。為了能夠安裝新庫,還必須安裝CabalStack等工具。如果您運行的是類Unix操作系統,最簡單的安裝是使用以下命令安裝Stack

curl -sSL https://get.haskellstack.org/ | sh
 

這會將GHC與系統的其他部分隔離開來,因此很容易刪除。但是所有命令必須以stack 開頭。另一種簡單的方法是安裝Haskell平台 。該平台有兩種形式:

  1. 最小分佈僅包含GHC (編譯)和Cabal / Stack (安裝和構建包)
  2. 完整分發還包含用於項目開發,分析​​和覆蓋分析的工具。還包括另外一組廣泛使用的包。

可以通過下載安裝程序並按照說明或使用您的發行版的軟件包管理器來安裝這些平台(請注意,此版本不保證是最新的):

  • Ubuntu,Debian,Mint:

    sudo apt-get install haskell-platform
     
  • Fedora的:

    sudo dnf install haskell-platform
     
  • 紅帽:

    sudo yum install haskell-platform
     
  • Arch Linux:

    sudo pacman -S ghc cabal-install haskell-haddock-api \
                   haskell-haddock-library happy alex
     
  • Gentoo的:

    sudo layman -a haskell
    sudo emerge haskell-platform
     
  • OSX與Homebrew:

    brew cask install haskell-platform
     
  • OSX與MacPorts:

    sudo port install haskell-platform
     

安裝後,應該可以通過在終端中的任何位置調用ghci 命令來啟動GHCi 。如果安裝順利,控制台應該看起來像

me@notebook:~$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/  :? for help
Prelude> 
 

可能還有一些關於在Prelude> 之前加載了哪些庫的信息。現在,控制台已成為Haskell REPL,您可以像在線REPL一樣執行Haskell代碼。要退出此交互式環境,可以鍵入:q:quit 。有關GHCi中可用命令的更多信息,請鍵入:? 如開始屏幕所示。

因為在一行上一次又一次地寫相同的東西並不總是這樣,所以在文件中編寫Haskell代碼可能是個好主意。這些文件通常具有.hs 作為擴展名,可以使用:l:load 加載到REPL中。

如前所述, GHCiGHC的一部分, GHC實際上是一個編譯器。此編譯器可用於將帶有Haskell代碼的.hs 文件轉換為正在運行的程序。由於.hs 文件可以包含許多函數,因此必須在文件中定義main 函數。這將是該計劃的起點。可以使用該命令編譯文件test.hs

ghc test.hs
 

如果沒有錯誤並且main 函數被正確定義,這將創建目標文件和可執行文件。

更高級的工具

  1. 它之前已經被提到作為包管理器,但是堆棧可以以完全不同的方式用於Haskell開發的有用工具。一旦安裝,它就能夠

    • 安裝(多個版本) GHC
    • 項目創建和腳手架
    • 依賴管理
    • 建設和測試項目
    • 標杆
  2. IHaskell是IPythonhaskell內核 ,允許將(可運行的)代碼與markdown和數學符號結合起來。

宣布價值觀

我們可以在REPL中聲明一系列表達式,如下所示:

Prelude> let x = 5
Prelude> let y = 2 * 5 + x
Prelude> let result = y * 10
Prelude> x
5
Prelude> y
15
Prelude> result
150
 

要在文件中聲明相同的值,我們編寫以下內容:

-- demo.hs

module Demo where
-- We declare the name of our module so 
-- it can be imported by name in a project.

x = 5

y = 2 * 5 + x

result = y * 10
 

與變量名稱不同,模塊名稱是大寫的。

階乘

階乘函數是Haskell“Hello World!” (通常用於函數式編程),它簡潔地演示了該語言的基本原理。

變化1

fac :: (Integral a) => a -> a
fac n = product [1..n]
 

現場演示

  • Integral 是整數類型的類。示例包括IntInteger
  • (Integral a) => 對所述類中的類型a 施加約束
  • fac :: a -> a 說, fac 是需要一個功能a 並返回a
  • product 是一個函數,通過將它們相乘來累積列表中的所有數字。
  • [1..n] 是特殊的符號,其desugars到enumFromTo 1 n ,並且是數的範圍1 ≤ x ≤ n

變化2

fac :: (Integral a) => a -> a
fac 0 = 1
fac n = n * fac (n - 1)
 

現場演示

此變體使用模式匹配將函數定義拆分為單獨的案例。如果參數為0 (有時稱為停止條件),則調用第一個定義,否則調用第二個定義(定義的順序很重要)。它還舉例說明了fac 引用自身的遞歸。


值得注意的是,由於重寫規則,當使用GHC並激活優化時,兩個版本的fac 將編譯為相同的機器代碼。因此,就效率而言,兩者是等價的。

Fibonacci,使用惰性評估

延遲評估意味著Haskell將僅評估其值需要的列表項。

基本的遞歸定義是:

f (0)  <-  0
f (1)  <-  1
f (n)  <-  f (n-1) + f (n-2)
 

如果直接評估,它將非常慢。但是,假設我們有一個記錄所有結果的列表,

fibs !! n  <-  f (n) 
 

然後

                  ┌──────┐   ┌──────┐   ┌──────┐
                  │ f(0) │   │ f(1) │   │ f(2) │
fibs  ->  0 : 1 : │  +   │ : │  +   │ : │  +   │ :  .....
                  │ f(1) │   │ f(2) │   │ f(3) │
                  └──────┘   └──────┘   └──────┘

                  ┌────────────────────────────────────────┐
                  │ f(0)   :   f(1)   :   f(2)   :  .....  │ 
                  └────────────────────────────────────────┘
      ->  0 : 1 :               +
                  ┌────────────────────────────────────────┐
                  │ f(1)   :   f(2)   :   f(3)   :  .....  │
                  └────────────────────────────────────────┘
 

這被編碼為:

fibn n = fibs !! n
    where
    fibs = 0 : 1 : map f [2..]
    f n = fibs !! (n-1) + fibs !! (n-2)
 

甚至是

GHCi> let fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 

zipWith 通過將給定的二進制函數應用於給定的兩個列表的相應元素來生成列表,因此zipWith (+) [x1, x2, ...] [y1, y2, ...] 等於[x1 + y1, x2 + y2, ...]

編寫fibs 另一種方法是使用scanl 函數

GHCi> let fibs = 0 : scanl (+) 1 fibs
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 

scanl 構建foldl 將產生的部分結果列表,沿著輸入列表從左到右工作。也就是說, scanl f z0 [x1, x2, ...] 等於[z0, z1, z2, ...] where z1 = f z0 x1; z2 = f z1 x2; ...

由於延遲評估,兩個函數都定義了無限列表,而沒有完全計算出來。也就是說,我們可以編寫一個fib 函數,檢索無界Fibonacci序列的第n個元素:

GHCi> let fib n = fibs !! n  -- (!!) being the list subscript operator
-- or in point-free style:
GHCi> let fib = (fibs !!)
GHCi> fib 9
34
 

你好,世界!

一個基本的“你好,世界!” Haskell中的程序可以用一兩行簡潔地表達出來:

main :: IO ()
main = putStrLn "Hello, World!"
 

第一行是可選類型註釋,表示mainIO () 類型的值,表示“計算”type () 值的I / O操作(讀取“unit”;空元組不傳遞信息)除了對外界施加一些副作用(這裡,在終端打印一個字符串)。 main 通常省略此類型註釋,因為它是唯一可能的類型。

將它放入helloworld.hs 文件並使用Haskell編譯器(如GHC)編譯它:

ghc helloworld.hs
 

執行編譯的文件將導致輸出"Hello, World!" 正在打印到屏幕上:

./helloworld
Hello, World!
 

或者, runhaskellrunghc 使得可以在解釋模式下運行程序而無需編譯它:

runhaskell helloworld.hs
 

也可以使用交互式REPL而不是編譯。它隨大多數Haskell環境一起提供,例如GHC編譯器附帶的ghci

ghci> putStrLn "Hello World!"
Hello, World!
ghci> 
 

或者,使用load (或:l )從文件加載腳本到ghci:

ghci> :load helloworld
 

:reload (或:r )重新加載ghci中的所有內容:

Prelude> :l helloworld.hs 
[1 of 1] Compiling Main             ( helloworld.hs, interpreted )

<some time later after some edits>

*Main> :r
Ok, modules loaded: Main.
 

說明:

第一行是類型簽名,聲明main 的類型:

main :: IO ()
 

IO () 類型的值描述了可以與外部世界交互的動作。

因為Haskell有一個完全成熟的Hindley-Milner類型系統允許自動類型推斷,所以類型簽名在技術上是可選的:如果你只是省略main :: IO () ,編譯器將能夠通過它自己推斷出類型。分析main定義 。但是,如果不為頂級定義編寫類型簽名,則被認為是不好的樣式。原因包括:

  • Haskell中的類型簽名是一個非常有用的文檔,因為類型系統是如此富有表現力,以至於您通常只需查看其類型就可以看到函數有什麼好處。可以使用GHCi等工具方便地訪問此“文檔”。與普通文檔不同,編譯器的類型檢查器將確保它實際匹配函數定義!

  • 類型簽名將bug保持為本地 。如果你在定義中犯了一個錯誤而沒有提供它的類型簽名,編譯器可能不會立即報告錯誤,而是簡單地為它推斷出一個無意義的類型,實際上它可以用來檢測它。然後,您可能會在使用該值時收到一條神秘的錯誤消息。通過簽名,編譯器非常善於發現錯誤。

第二行是實際工作:

main = putStrLn "Hello, World!"
 

如果您來自命令式語言,請注意此定義也可以寫成:

main = do {
   putStrLn "Hello, World!" ;
   return ()
   }
 

或者等價(Haskell具有基於佈局的解析;但要注意混合製表符和空格不一致會混淆這種機制):

main = do
    putStrLn "Hello, World!"
    return ()
 

do 塊中的每一行代表一些monadic (此處為I / O) 計算 ,因此整個do 塊表示由這些子步驟組成的整體操作,方法是以特定於給定monad的方式組合它們(用於I / O)這意味著一個接一個地執行它們。

do 語法本身就是monad的語法糖,就像這裡的IO 一樣, return 是一個無操作的動作,產生它的參數而不會執行任何副作用或額外的計算,這可能是特定monad定義的一部分。

以上與定義main = putStrLn "Hello, World!" ,因為值putStrLn "Hello, World!" 已經有類型IO () 。作為“聲明”, putStrLn "Hello, World!" 可以看作是一個完整的程序,你只需定義main 來引用這個程序。

您可以在線查找putStrLn 的簽名

putStrLn :: String -> IO ()
-- thus,
putStrLn (v :: String) :: IO ()
 

putStrLn 是一個以字符串作為參數並輸出I / O操作(即表示運行時可以執行的程序的值)的函數。運行時始終執行名為main 的操作,因此我們只需將其定義為等於putStrLn "Hello, World!"

素數

一些最突出的變種:

低於100

import Data.List ( (\\) )

ps100 = ((([2..100] \\ [4,6..100]) \\ [6,9..100]) \\ [10,15..100]) \\ [14,21..100]

   -- = (((2:[3,5..100]) \\ [9,15..100]) \\ [25,35..100]) \\ [49,63..100]

   -- = (2:[3,5..100]) \\ ([9,15..100] ++ [25,35..100] ++ [49,63..100])
 

無限

Eratosthenes篩選,使用數據ordlist包

import qualified Data.List.Ordered

ps   = 2 : _Y ((3:) . minus [5,7..] . unionAll . map (\p -> [p*p, p*p+2*p..]))

_Y g = g (_Y g)   -- = g (g (_Y g)) = g (g (g (g (...)))) = g . g . g . g . ...
 

傳統

(次優試驗篩)

ps = sieve [2..]
     where
     sieve (x:xs) = [x] ++ sieve [y | y <- xs, rem y x > 0]

-- = map head ( iterate (\(x:xs) -> filter ((> 0).(`rem` x)) xs) [2..] )
 

最佳試驗分工

ps = 2 : [n | n <- [3..], all ((> 0).rem n) $ takeWhile ((<= n).(^2)) ps]

-- = 2 : [n | n <- [3..], foldr (\p r-> p*p > n || (rem n p > 0 && r)) True ps]
 

過渡

從試驗師到篩選Eratosthenes:

[n | n <- [2..], []==[i | i <- [2..n-1], j <- [0,i..n], j==n]]
 

最短的代碼

nubBy (((>1).).gcd) [2..]          -- i.e., nubBy (\a b -> gcd a b > 1) [2..]
 

nubBy來自Data.List ,就像(\\)