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 ,就像(\\)