Haskell 快速指南



Haskell - 概述

Haskell 是一种函数式编程语言,专门设计用于处理符号计算和列表处理应用程序。函数式编程基于数学函数。除了 Haskell,其他一些遵循函数式编程范式的流行语言包括:Lisp、Python、Erlang、Racket、F#、Clojure 等。

传统编程中,指令被视为特定语法或格式的一组声明,但在函数式编程中,所有计算都被视为单独数学函数的组合。

使用 Haskell 进行函数式编程

Haskell 是一种广泛使用的纯函数式语言。这里,我们列出了一些使 Haskell 比其他传统编程语言(如 Java、C、C++、PHP 等)更特殊的几点。

  • 函数式语言 - 在传统编程语言中,我们指示编译器一系列任务,这仅仅是告诉你的计算机“做什么”和“如何做?”。但在 Haskell 中,我们将告诉计算机“它是什么?”

  • 惰性求值 - Haskell 是一种惰性语言。惰性意味着 Haskell 不会无缘无故地计算任何表达式。当求值引擎发现需要计算某个表达式时,它会创建一个thunk 数据结构来收集该特定求值所需的所有信息以及指向该thunk 数据结构的指针。只有当需要计算该特定表达式时,求值引擎才会开始工作。

  • 模块化 - Haskell 应用程序只是一系列函数。我们可以说 Haskell 应用程序是许多小型 Haskell 应用程序的集合。

  • 静态类型 - 在传统编程语言中,我们需要定义一系列变量及其类型。相反,Haskell 是一种类型推断语言。类型推断语言意味着 Haskell 编译器足够智能,可以推断出声明的变量的类型,因此我们不需要显式提及所用变量的类型。

  • 可维护性 - Haskell 应用程序是模块化的,因此维护它们非常容易且经济高效。

函数式程序更具并发性,并且遵循并行执行以提供更准确和更好的性能。Haskell 也不例外;它已被开发用于有效地处理多线程

Hello World

这是一个简单的例子,用于演示 Haskell 的动态性。请看下面的代码。我们只需要一行代码就可以在控制台上打印“Hello World”。

main = putStrLn "Hello World"

一旦 Haskell 编译器遇到上述代码段,它就会立即产生以下输出:

Hello World 

我们将在这个教程中提供大量的例子来展示 Haskell 的强大和简单性。

Haskell - 环境搭建

我们在以下网站上设置了在线 Haskell 编程环境:https://tutorialspoint.com/compile_haskell_online.php

这个在线编辑器有很多选项可以练习 Haskell 编程示例。转到页面的终端部分并键入“ghci”。此命令会自动加载 Haskell 编译器并启动在线 Haskell。使用ghci命令后,您将收到以下输出。

sh-4.3$ ghci
GHCi,version7.8.4:http://www.haskell.org/ghc/:?forhelp
Loading package ghc-prim...linking...done.
Loading packageinteger gmp...linking... done.
Loading package base...linking...done.
Prelude>

如果您仍然想在本地系统中离线使用 Haskell,则需要从其官方网页下载可用的 Haskell 安装程序:https://www.haskell.org/downloads

市场上有三种不同类型的安装程序

  • 最小安装程序 - 它提供 GHC(Glasgow Haskell 编译器)、CABAL(应用程序和库的通用架构)和 Stack 工具。

  • Stack 安装程序 - 在此安装程序中,GHC 可以跨平台下载到受管理的工具链中。它将在全局安装您的应用程序,以便它可以在需要时更新其 API 工具。它会自动解决所有面向 Haskell 的依赖项。

  • Haskell 平台 - 这是安装 Haskell 的最佳方法,因为它会将整个平台安装到您的机器上,并且是从一个特定位置安装的。此安装程序不像上面两个安装程序那样是可分发的。

我们已经看到了市场上不同类型的安装程序,现在让我们看看如何在我们的机器上使用这些安装程序。在本教程中,我们将使用 Haskell 平台安装程序在我们的系统中安装 Haskell 编译器。

Windows 环境搭建

要在 Windows 计算机上设置 Haskell 环境,请访问其官方网站https://www.haskell.org/platform/windows.html并根据您的可定制架构下载安装程序。

Windows Installer

查看您系统的架构并下载相应的安装文件并运行它。它将像任何其他 Windows 应用程序一样安装。您可能需要更新系统的 CABAL 配置。

MAC 环境搭建

要在 MAC 系统上设置 Haskell 环境,请访问其官方网站https://www.haskell.org/platform/mac.html并下载 Mac 安装程序。

MAC Installer

Linux 环境搭建

在基于 Linux 的系统上安装 Haskell 需要运行一些命令,这不像 MAC 和 Windows 那样容易。是的,它很费力,但它是可靠的。

您可以按照以下步骤在 Linux 系统上安装 Haskell:

步骤 1 - 要在 Linux 系统上设置 Haskell 环境,请访问官方网站https://www.haskell.org/platform/linux.html并选择您的发行版。您将在浏览器上看到以下屏幕。

Linux Installer

步骤 2 - 选择您的发行版。在本例中,我们使用 Ubuntu。选择此选项后,您将在屏幕上看到以下页面,其中包含在本地系统中安装 Haskell 的命令。

Ubuntu Installer

步骤 3 - 按 Ctrl + Alt + T 打开终端。运行命令“$ sudo apt-get install haskell-platform”并按 Enter。在使用 root 密码进行身份验证后,它将自动开始在您的系统上下载 Haskell。安装完成后,您将收到确认消息。

步骤 4 - 再次进入终端并运行 GHCI 命令。一旦您得到 Prelude 提示符,您就可以在本地系统上使用 Haskell 了。

Local System

要退出 GHCI 提示符,可以使用命令“:quit exit”。

Haskell - 基本数据模型

Haskell 是一种纯函数式编程语言,因此它比其他编程语言更具交互性和智能性。在本章中,我们将学习 Haskell 的基本数据模型,这些模型实际上是预定义的,或者以某种方式巧妙地解码到计算机内存中。

在本教程中,我们将使用我们网站上提供的在线 Haskell 平台(https://tutorialspoint.com/codingground.htm)。

数字

Haskell 足够智能,可以将一些数字解码为数字。因此,您不需要像在其他编程语言中通常那样从外部提及其类型。例如,转到您的 prelude 命令提示符并运行“2+2”,然后按 Enter。

sh-4.3$ ghci 
GHCi, version 7.6.3: http://www.haskell.org/ghc/  :? for help 
Loading package ghc-prim ... linking ... done. 
Loading package integer-gmp ... linking ... done. 
Loading package base ... linking ... done. 
Prelude> 2+2

您将收到以下输出作为结果。

4

在上面的代码中,我们只是将两个数字作为参数传递给 GHCI 编译器而没有预定义它们的类型,但编译器可以轻松地将这两个条目解码为数字。

现在,让我们尝试更复杂的数学计算,看看我们的智能编译器是否能给我们正确的输出。尝试使用“15+(5*5)-40”。

Prelude> 15+(5*5)-40 

根据预期输出,上述表达式产生“0”。

0

字符

像数字一样,Haskell 可以智能地识别作为输入给它的字符。转到您的 Haskell 命令提示符并键入带双引号或单引号的任何字符。

让我们提供以下行作为输入并检查其输出。

Prelude> :t "a" 

它将产生以下输出:

"a" :: [Char] 

记住在提供输入时使用 (:t)。在上面的示例中,(:t) 用于包含与输入相关的特定类型。我们将在接下来的章节中更多地了解这种类型。

让我们来看下面的例子,我们将一些无效的输入作为字符传递,这反过来会导致错误。

Prelude> :t a 
<interactive>:1:1: Not in scope: 'a'  

Prelude> a 
<interactive>:4:1: Not in scope: 'a' 

通过错误消息“<interactive>:4:1: Not in scope: `a'”,Haskell 编译器警告我们它无法识别您的输入。Haskell 是一种所有内容都使用数字表示的语言。

Haskell 遵循传统的 ASCII 编码风格。让我们来看下面的例子,以了解更多信息:

Prelude> '\97' 
'a'  
Prelude> '\67' 
'C' 

看看您的输入是如何解码成 ASCII 格式的。

字符串

字符串只是一组字符。使用字符串没有具体的语法,但 Haskell 遵循用双引号表示字符串的传统风格。

让我们来看下面的例子,我们将传递字符串“Tutorialspoint.com”。

Prelude> :t "tutorialspoint.com" 

它将在屏幕上产生以下输出:

"tutorialspoint.com" :: [Char] 

看看整个字符串是如何被解码为仅由 Char 组成的数组的。让我们转向其他数据类型及其语法。一旦我们开始实际练习,我们将习惯所有数据类型及其用法。

布尔值

布尔数据类型与其他数据类型一样非常直接。看看下面的例子,我们将使用一些布尔输入(如“True”或“False”)使用不同的布尔运算。

Prelude> True && True 
True  
Prelude> True && False 
False   
Prelude> True || True 
True  
Prelude> True || False 
True

在上面的例子中,我们不需要提到“True”和“False”是布尔值。Haskell 本身可以解码它并执行相应的运算。让我们用“true”或“false”修改我们的输入。

Prelude> true 

它将产生以下输出:

<interactive>:9:1: Not in scope: 'true' 

在上面的例子中,Haskell 无法区分“true”和数值,因此我们的输入“true”不是数字。因此,Haskell 编译器抛出一个错误,指出我们的输入不在其范围内。

列表和列表推导

像其他数据类型一样,列表 (List) 也是 Haskell 中非常有用的数据类型。例如,[a,b,c] 是一个字符列表,因此,根据定义,列表是由逗号分隔的相同数据类型的集合。

与其他数据类型一样,您不需要声明列表为列表。Haskell 足够智能,可以根据表达式中使用的语法来解码您的输入。

让我们来看下面的例子,它展示了 Haskell 如何处理列表。

Prelude> [1,2,3,4,5] 

它将产生以下输出:

[1,2,3,4,5] 

Haskell 中的列表是同构的,这意味着它们不允许您声明不同类型数据的列表。任何像 [1,2,3,4,5,a,b,c,d,e,f] 这样的列表都会产生错误。

Prelude> [1,2,3,4,5,a,b,c,d,e,f] 

这段代码将产生以下错误:

<interactive>:17:12: Not in scope: 'a' 
<interactive>:17:14: Not in scope: 'b' 
<interactive>:17:16: Not in scope: 'c' 
<interactive>:17:18: Not in scope: 'd' 
<interactive>:17:20: Not in scope: 'e' 
<interactive>:17:22: Not in scope: 'f'

列表推导式 (List Comprehension)

列表推导式是使用数学表达式生成列表的过程。看下面的例子,我们正在使用“[输出 | 范围,条件]”格式的数学表达式生成列表。

Prelude> [x*2| x<-[1..10]] 
[2,4,6,8,10,12,14,16,18,20]  
Prelude> [x*2| x<-[1..5]] 
[2,4,6,8,10]  
Prelude> [x| x<-[1..5]] 
[1,2,3,4,5]

这种使用数学表达式创建列表的方法称为列表推导式 (List Comprehension)

元组 (Tuple)

Haskell 提供了另一种在单个数据类型中声明多个值的方法。它被称为元组 (Tuple)。元组可以被认为是列表,但是元组和列表之间存在一些技术差异。

元组是不可变的数据类型,我们不能在运行时修改元素的数量,而列表是可变的数据类型。

另一方面,列表是同构的数据类型,但元组是异构的,因为元组可以包含不同类型的数据。

元组由单括号表示。请看下面的例子,了解 Haskell 如何处理元组。

Prelude> (1,1,'a') 

它将产生以下输出:

(1,1,'a') 

在上面的例子中,我们使用了一个包含两个数字类型变量和一个字符类型变量的元组。

Haskell - 基本运算符

在本章中,我们将学习 Haskell 中使用的不同运算符。与其他编程语言一样,Haskell 智能地处理一些基本的运算,如加法、减法、乘法等。在接下来的章节中,我们将学习更多关于不同运算符及其用法的知识。

在本章中,我们将使用我们的在线平台 (https://tutorialspoint.com/codingground.htm) 中 Haskell 的不同运算符。请记住,我们只使用整数类型数字,因为我们将在后续章节中学习更多关于小数类型数字的知识。

加法运算符

顾名思义,加法运算符 (+) 用于加法运算。以下示例代码展示了如何在 Haskell 中添加两个整数:

main = do 
   let var1 = 2 
   let var2 = 3 
   putStrLn "The addition of the two numbers is:" 
   print(var1 + var2) 

在上面的文件中,我们创建了两个单独的变量var1var2。最后,我们使用加法运算符打印结果。使用编译执行按钮运行您的代码。

这段代码将在屏幕上产生以下输出:

The addition of the two numbers is:
5

减法运算符

顾名思义,此运算符用于减法运算。以下示例代码展示了如何在 Haskell 中减去两个整数:

main = do 
   let var1 = 10 
   let var2 = 6 
   putStrLn "The Subtraction of the two numbers is:" 
   print(var1 - var2)

在这个例子中,我们创建了两个变量var1var2。然后,我们使用减法 (-) 运算符来减去这两个值。

这段代码将在屏幕上产生以下输出:

The Subtraction of the two numbers is:
4

乘法运算符

此运算符用于乘法运算。以下代码展示了如何使用乘法运算符在 Haskell 中乘以两个数字:

main = do 
   let var1 = 2 
   let var2 = 3 
   putStrLn "The Multiplication of the Two Numbers is:" 
   print(var1 * var2) 

当您在我们的在线平台上运行此代码时,它将产生以下输出:

The Multiplication of the Two Numbers is:
6 

除法运算符

请看下面的代码。它展示了如何在 Haskell 中除以两个数字:

main = do 
   let var1 = 12 
   let var2 = 3 
   putStrLn "The Division of the Two Numbers is:" 
   print(var1/var2)

它将产生以下输出:

The Division of the Two Numbers is: 
4.0 

序列/范围运算符

序列或范围是 Haskell 中的一个特殊运算符。它用 "(..)" 表示。在声明具有值序列的列表时,可以使用此运算符。

如果您想打印从 1 到 10 的所有值,那么您可以使用类似“[1..10]”的东西。类似地,如果您想生成从“a”到“z”的所有字母,那么您只需键入“[a..z]”

以下代码展示了如何使用序列运算符打印从 1 到 10 的所有值:

main :: IO() 
main = do 
   print [1..10]

它将生成以下输出:

[1,2,3,4,5,6,7,8,9,10] 

Haskell - 决策

决策机制允许程序员在代码流程中应用条件。程序员可以根据预定义的条件执行一组指令。以下流程图显示了 Haskell 的决策机制结构:

Condition Loop

Haskell 提供以下类型的决策语句:

序号 语句及描述
1 if-else 语句

一个if 语句和一个else 语句。只有当给定的布尔条件不满足时,else 块中的指令才会执行。

2 嵌套 if-else 语句

多个if 块后跟else

Haskell - 类型和类型类

Haskell 是一种函数式语言,它是强类型语言,这意味着整个应用程序中使用的数据类型在编译时将为编译器所知。

内置类型类

在 Haskell 中,每个语句都被视为一个数学表达式,此表达式的类别称为类型。您可以说“类型”是在编译时使用的表达式的数 据类型。

为了更多地了解类型,我们将使用“:t”命令。一般来说,类型可以被认为是一个值,而类型类可以被认为是一组类似的类型。在本章中,我们将学习不同的内置类型。

Int

Int 是表示整数类型数据的类型类。从 -2147483647 到 2147483647 之间的每个整数都属于Int 类型类。在下面的例子中,函数fType() 将根据其定义的类型进行操作。

fType :: Int -> Int -> Int 
fType x y = x*x + y*y
main = print (fType 2 4) 

在这里,我们将函数fType() 的类型设置为int。该函数接受两个int 值并返回一个int 值。如果您编译并执行这段代码,它将产生以下输出:

sh-4.3$ ghc -O2 --make *.hs -o main -threaded -rtsopts 
sh-4.3$ main
20

Integer

Integer 可以被认为是Int 的超集。此值不受任何数字限制,因此 Integer 可以是任意长度,没有任何限制。为了了解IntInteger 类型之间的基本区别,让我们修改上面的代码如下:

fType :: Int -> Int -> Int 
fType x y = x*x + y*y 
main = print (fType 212124454 44545454454554545445454544545)

如果您编译上面的代码,将抛出以下错误消息:

main.hs:3:31: Warning:            
   Literal 44545454454554545445454544545 is out of the Int range -
   9223372036854775808..9223372036854775807 
Linking main ...

发生此错误是因为我们的函数 fType() 期望一个 Int 类型值,而我们正在传递一些非常大的 Int 类型值。为了避免此错误,让我们将类型“Int”修改为“Integer”并观察差异。

fType :: Integer -> Integer -> Integer 
fType x y = x*x + y*y 
main = print (fType 212124454 4454545445455454545445445454544545) 

现在,它将产生以下输出:

sh-4.3$ main
1984297512562793395882644631364297686099210302577374055141

Float

请看下面的代码。它展示了 Float 类型在 Haskell 中是如何工作的:

fType :: Float -> Float -> Float 
fType x y = x*x + y*y 
main = print (fType 2.5 3.8)

该函数将两个浮点值作为输入,并产生另一个浮点值作为输出。当您编译并执行此代码时,它将产生以下输出:

sh-4.3$ main
20.689999 

Double

Double 是一个浮点数,最后具有双精度。请看下面的例子:

fType :: Double -> Double -> Double 
fType x y = x*x + y*y 
main = print (fType 2.56 3.81)

当您执行上面的代码时,它将生成以下输出:

sh-4.3$ main 
21.0697

Bool

Bool 是布尔类型。它可以是 True 或 False。执行以下代码以了解 Bool 类型在 Haskell 中是如何工作的:

main = do  
   let x = True 
   
   if x == False 
      then putStrLn "X matches with Bool Type" 
   else putStrLn "X is not a Bool Type" 

在这里,我们将变量“x”定义为 Bool,并将其与另一个布尔值进行比较以检查其真伪。它将产生以下输出:

sh-4.3$ main
X is not a Bool Type 

Char

Char 表示字符。单引号内的任何内容都被视为字符。在下面的代码中,我们修改了之前的fType() 函数,使其接受 Char 值并返回 Char 值作为输出。

fType :: Char-> Char 
fType x = 'K' 
main = do  
   let x = 'v' 
   print (fType x) 

上面的代码将使用 'v' 的char 值调用fType() 函数,但它返回另一个 char 值,即 'K'。这是它的输出:

sh-4.3$ main 
'K'

请注意,我们不会显式地使用这些类型,因为 Haskell 足够智能,可以在声明之前捕获类型。在本教程的后续章节中,我们将看到不同的类型和类型类是如何使 Haskell 成为一种强类型语言的。

EQ 类型类

EQ 类型类是一个接口,它提供测试表达式相等性的功能。任何想要检查表达式相等性的类型类都应该属于此 EQ 类型类。

上面提到的所有标准类型类都是此EQ 类的一部分。每当我们使用上面提到的任何类型检查任何相等性时,实际上我们都在调用EQ 类型类。

在下面的例子中,我们使用“==”或“/=”运算内部使用EQ 类型。

main = do 
   if 8 /= 8 
      then putStrLn "The values are Equal" 
   else putStrLn "The values are not Equal"

它将产生以下输出:

sh-4.3$ main 
The values are not Equal 

Ord 类型类

Ord 是另一个接口类,它为我们提供了排序的功能。到目前为止我们使用过的所有类型都是此Ord 接口的一部分。像 EQ 接口一样,Ord 接口可以使用“>”、“<”、“<=”、“>=”、“compare”来调用。

请参见下面的示例,我们使用了此类型类的“compare”功能。

main = print (4 <= 2) 

在这里,Haskell 编译器将检查 4 是否小于或等于 2。由于它不是,代码将产生以下输出:

sh-4.3$ main 
False

Show

Show 的功能是将其参数打印为字符串。无论其参数是什么,它总是将结果打印为字符串。在下面的示例中,我们将使用此接口打印整个列表。“show”可用于调用此接口。

main = print (show [1..10]) 

它将在控制台上产生以下输出。这里,双引号表示它是一个字符串类型的值。

sh-4.3$ main 
"[1,2,3,4,5,6,7,8,9,10]" 

Read

Read 接口与 Show 做相同的事情,但它不会以字符串格式打印结果。在下面的代码中,我们使用了read 接口来读取字符串值并将相同的值转换为 Int 值。

main = print (readInt "12") 
readInt :: String -> Int 
readInt = read 

在这里,我们将字符串变量(“12”)传递给readInt 方法,该方法在转换后返回 12(一个 Int 值)。这是它的输出:

sh-4.3$ main 
12

Enum

Enum 是另一种类型的类型类,它在 Haskell 中启用顺序功能。此类型类可以通过Succ、Pred、Bool、Char 等命令访问。

以下代码展示了如何查找 12 的后继值。

main = print (succ 12) 

它将产生以下输出:

sh-4.3$ main
13

Bounded

所有具有上限和下限的类型都属于此类型类。例如,Int 类型数据具有“9223372036854775807”的最大边界和“-9223372036854775808”的最小边界。

以下代码展示了 Haskell 如何确定 Int 类型的最大和最小边界。

main = do 
   print (maxBound :: Int) 
   print (minBound :: Int) 

它将产生以下输出:

sh-4.3$ main
9223372036854775807
-9223372036854775808

现在,尝试查找 Char、Float 和 Bool 类型的最大和最小边界。

Num

此类型类用于数值运算。Int、Integer、Float 和 Double 等类型属于此类型类。请看下面的代码:

main = do 
   print(2 :: Int)  
   print(2 :: Float) 

它将产生以下输出:

sh-4.3$ main
2
2.0

Integral

Integral 可以被认为是 Num 类型类的子类。Num 类型类包含所有类型的数字,而 Integral 类型类仅用于整数。Int 和 Integer 是此类型类下的类型。

Floating

与 Integral 一样,Floating 也是 Num 类型类的一部分,但它只包含浮点数。因此,FloatDouble 属于此类型类。

自定义类型类

像任何其他编程语言一样,Haskell 允许开发人员定义用户定义的类型。在下面的示例中,我们将创建一个用户定义的类型并使用它。

data Area = Circle Float Float Float  
surface :: Area -> Float   
surface (Circle _ _ r) = pi * r ^ 2   
main = print (surface $ Circle 10 20 10 ) 

这里,我们创建了一个名为Area的新类型。接下来,我们将使用此类型来计算圆的面积。在上面的示例中,“surface”是一个函数,它以Area作为输入,并产生Float作为输出。

请记住,“data”在这里是一个关键字,所有 Haskell 中的用户定义类型都以大写字母开头。

它将产生以下输出:

sh-4.3$ main
314.15927

Haskell - 函数

函数在 Haskell 中扮演着重要角色,因为它是一种函数式编程语言。与其他语言一样,Haskell 也有自己的函数定义和声明。

  • 函数声明包括函数名及其参数列表以及其输出。

  • 函数定义是在你实际定义函数的地方。

让我们以add函数为例,详细了解这个概念。

add :: Integer -> Integer -> Integer   --function declaration 
add x y =  x + y                       --function definition 

main = do 
   putStrLn "The addition of the two numbers is:"  
   print(add 2 5)    --calling a function 

在这里,我们在第一行声明了我们的函数,在第二行,我们编写了实际的函数,它将接受两个参数并产生一个整型输出。

与大多数其他语言一样,Haskell 从main方法开始编译代码。我们的代码将生成以下输出:

The addition of the two numbers is:
7

模式匹配

模式匹配是匹配特定类型表达式的过程。它只是一种简化代码的技术。这种技术可以实现到任何类型的类型类中。可以使用 If-Else 作为模式匹配的替代选项。

模式匹配可以被认为是动态多态性的一种变体,在运行时,可以根据参数列表执行不同的方法。

请看下面的代码块。在这里,我们使用了模式匹配技术来计算一个数的阶乘。

fact :: Int -> Int 
fact 0 = 1 
fact n = n * fact ( n - 1 ) 

main = do 
   putStrLn "The factorial of 5 is:" 
   print (fact 5)

我们都知道如何计算一个数的阶乘。编译器将开始搜索一个名为“fact”且带有一个参数的函数。如果参数不等于 0,则该数字将继续调用具有比实际参数少 1 的相同函数。

当参数的模式与 0 完全匹配时,它将调用我们的模式“fact 0 = 1”。我们的代码将产生以下输出:

The factorial of 5 is:
120

守卫(Guards)

守卫是一个与模式匹配非常相似的概念。在模式匹配中,我们通常匹配一个或多个表达式,但我们使用守卫来测试表达式的某些属性。

虽然建议使用模式匹配而不是守卫,但从开发者的角度来看,守卫更易读且更简单。对于第一次使用的用户来说,守卫可能看起来与 If-Else 语句非常相似,但它们在功能上是不同的。

在下面的代码中,我们使用守卫的概念修改了我们的阶乘程序。

fact :: Integer -> Integer 
fact n | n == 0 = 1 
       | n /= 0 = n * fact (n-1) 
main = do 
   putStrLn "The factorial of 5 is:"  
   print (fact 5) 

在这里,我们声明了两个守卫,用 "|" 分隔,并从main调用fact函数。在内部,编译器的工作方式与模式匹配的情况相同,以产生以下输出:

The factorial of 5 is:
120

Where 子句

Where是一个关键字或内置函数,可以在运行时使用以生成所需的输出。当函数计算变得复杂时,它非常有用。

考虑这样一种情况:你的输入是一个包含多个参数的复杂表达式。在这种情况下,可以使用“where”子句将整个表达式分解成小的部分。

在下面的示例中,我们采用了一个复杂的数学表达式。我们将展示如何使用 Haskell 求解多项式方程 [x^2 - 8x + 6] 的根。

roots :: (Float, Float, Float) -> (Float, Float)  
roots (a,b,c) = (x1, x2) where 
   x1 = e + sqrt d / (2 * a) 
   x2 = e - sqrt d / (2 * a) 
   d = b * b - 4 * a * c  
   e = - b / (2 * a)  
main = do 
   putStrLn "The roots of our Polynomial equation are:" 
   print (roots(1,-8,6))

注意我们计算给定多项式函数的根的表达式的复杂性。它相当复杂。因此,我们使用where子句来分解表达式。上面的代码将生成以下输出:

The roots of our Polynomial equation are:
(7.1622777,0.8377223)

递归函数

递归是一种函数反复调用自身的情况。Haskell 并没有提供任何循环任何表达式超过一次的机制。相反,Haskell 希望你将你的整个功能分解成一系列不同的函数,并使用递归技术来实现你的功能。

让我们再次考虑我们的模式匹配示例,其中我们计算了一个数的阶乘。寻找一个数的阶乘是使用递归的经典案例。在这里,你可能会问:“模式匹配与递归有什么不同?”这两者之间的区别在于它们的使用方式。模式匹配用于设置终端约束,而递归是一个函数调用。

在下面的示例中,我们同时使用了模式匹配和递归来计算 5 的阶乘。

fact :: Int -> Int 
fact 0 = 1 
fact n = n * fact ( n - 1 ) 

main = do 
   putStrLn "The factorial of 5 is:" 
   print (fact 5) 

它将产生以下输出:

The factorial of 5 is:
120

高阶函数

到目前为止,我们看到的是 Haskell 函数将一个类型作为输入,并产生另一个类型作为输出,这与其他命令式语言非常相似。高阶函数是 Haskell 的一个独特特性,你可以在其中使用函数作为输入或输出参数。

虽然这是一个虚拟概念,但在实际程序中,我们在 Haskell 中定义的每个函数都使用高阶机制来提供输出。如果你有机会查看 Haskell 的库函数,你会发现大多数库函数都是以高阶方式编写的。

让我们举一个例子,我们将导入一个内置的高阶函数 map 并使用它来根据我们的选择实现另一个高阶函数。

import Data.Char  
import Prelude hiding (map) 

map :: (a -> b) -> [a] -> [b] 
map _ [] = [] 
map func (x : abc) = func x : map func abc  
main = print $ map toUpper "tutorialspoint.com" 

在上面的示例中,我们使用了Char类型类的toUpper函数将我们的输入转换为大写。在这里,“map”方法将一个函数作为参数,并返回所需的输出。这是它的输出:

sh-4.3$ ghc -O2 --make *.hs -o main -threaded -rtsopts
sh-4.3$ main
"TUTORIALSPOINT.COM" 

Lambda 表达式

有时我们必须编写一个函数,该函数在应用程序的整个生命周期中只会被使用一次。为了处理这种情况,Haskell 开发人员使用另一个称为lambda 表达式lambda 函数的匿名块。

没有定义的函数称为 lambda 函数。lambda 函数用 "\" 字符表示。让我们看下面的例子,我们将输入值增加 1,而无需创建任何函数。

main = do 
   putStrLn "The successor of 4 is:"  
   print ((\x -> x + 1) 4)

在这里,我们创建了一个没有名称的匿名函数。它以整数 4 作为参数并打印输出值。我们基本上是在操作一个函数,甚至没有正确声明它。这就是 lambda 表达式的魅力。

我们的 lambda 表达式将产生以下输出:

sh-4.3$ main
The successor of 4 is:
5

Haskell - 函数详解

到目前为止,我们已经讨论了许多类型的 Haskell 函数,并使用了不同的方法来调用这些函数。在本章中,我们将学习一些基本的函数,这些函数可以很容易地在 Haskell 中使用,而无需导入任何特殊的类型类。大多数这些函数都是其他高阶函数的一部分。

Head 函数

Head函数作用于列表。它返回输入参数的第一个元素,该参数基本上是一个列表。在下面的示例中,我们传递一个包含 10 个值的列表,并且我们使用head函数生成该列表的第一个元素。

main = do 
   let x = [1..10]   
   putStrLn "Our list is:"  
   print (x) 
   putStrLn "The first element of the list is:" 
   print (head x)

它将产生以下输出:

Our list is: 
[1,2,3,4,5,6,7,8,9,10]
The first element of the list is:
1

Tail 函数

Tail是补充head函数的函数。它以列表作为输入,并产生整个列表,但不包括头部部分。这意味着tail函数返回整个列表,但不包括第一个元素。请看下面的示例:

main = do 
   let x = [1..10]   
   putStrLn "Our list is:"  
   print (x) 
   putStrLn "The tail of our list is:" 
   print (tail x) 

它将产生以下输出:

Our list is:
[1,2,3,4,5,6,7,8,9,10]
The tail of our list is:
[2,3,4,5,6,7,8,9,10]

Last 函数

顾名思义,它产生作为输入提供的列表的最后一个元素。查看下面的示例。

main = do 
   let x = [1..10]   
   putStrLn "Our list is:"  
   print (x) 
   putStrLn "The last element of our list is:" 
   print (last x)

它将产生以下输出:

Our list is:
[1,2,3,4,5,6,7,8,9,10]
The last element of our list is:
10

Init 函数

Init的作用与tail函数恰好相反。它以列表作为参数,并返回整个列表,但不包括最后一个条目。

main = do 
   let x = [1..10]   
   putStrLn "Our list is:"  
   print (x) 
   putStrLn "Our list without the last entry:"  
   print (init x) 

现在,观察它的输出:

Our list is:
[1,2,3,4,5,6,7,8,9,10]
Our list without the last entry:
[1,2,3,4,5,6,7,8,9]

Null 函数

Null是一个布尔检查函数,它作用于字符串,只有当给定的列表为空时才返回True,否则返回False。下面的代码检查提供的列表是否为空。

main = do 
   let x = [1..10]   
   putStrLn "Our list is:"  
   print (x) 
   putStrLn "Is our list empty?"  
   print (null x)

它将产生以下输出:

Our list is:
[1,2,3,4,5,6,7,8,9,10]
Is our list empty?
False

Reverse 函数

它作用于字符串输入,并将整个输入转换为反向顺序,并给出一个输出作为结果。以下是此函数的代码库。

main = do 
   let x = [1..10]  
   putStrLn "Our list is:" 
   print (x) 
   putStrLn "The list in Reverse Order is:" 
   print (reverse x)

它将产生以下输出:

Our list is:
[1,2,3,4,5,6,7,8,9,10]
The list in Reverse Order is:
[10,9,8,7,6,5,4,3,2,1]

Length 函数

此函数用于计算作为参数给定的列表的长度。请看下面的示例:

main = do 
   let x = [1..10]   
   putStrLn "Our list is:" 
   print (x) 
   putStrLn "The length of this list is:" 
   print (length x)

我们的列表中有 10 个元素,因此我们的代码将产生 10 作为输出。

Our list is:
[1,2,3,4,5,6,7,8,9,10]
The length of this list is:
10

Take 函数

Take函数用于从另一个字符串创建子字符串。下面的代码展示了如何在 Haskell 中使用 take 函数:

main = print(take 5 ([1 .. 10])) 

该代码从提供的列表中生成一个包含 5 个元素的子字符串:

[1,2,3,4,5]

Drop 函数

此函数也用于生成子字符串。它的功能与take函数相反。查看下面的代码:

main = print(drop 5 ([1 .. 10])) 

该代码从提供的列表中删除前 5 个元素,并打印其余 5 个元素。它将产生以下输出:

[6,7,8,9,10]

Maximum 函数

此函数用于从提供的列表中查找具有最大值的元素。让我们看看如何在实践中使用它:

main = do 
   let x = [1,45,565,1245,02,2]   
   putStrLn "The maximum value element of the list is:"  
   print (maximum x)

上面的代码将生成以下输出:

The maximum value element of the list is:
1245

Minimum 函数

此函数用于从提供的列表中查找具有最小值的元素。它只是maximum函数的反义词。

main = do 
   let x = [1,45,565,1245,02,2]   
   putStrLn "The minimum value element of the list is:"  
   print (minimum x)

上面代码的输出是:

The minimum value element of the list is:
1

Sum 函数

顾名思义,此函数返回提供的列表中所有元素的总和。下面的代码接受一个包含 5 个元素的列表,并将其总和作为输出返回。

main = do 
   let x = [1..5] 
   putStrLn "Our list is:" 
   print (x) 
   putStrLn "The summation of the list elements is:" 
   print (sum x)

它将产生以下输出:

Our list is:
[1,2,3,4,5]
The summation of the list elements is:
15

Product 函数

您可以使用此函数将列表中的所有元素相乘并打印其值。

main = do 
   let x = [1..5] 
   putStrLn "Our list is:" 
   print (x) 
   putStrLn "The multiplication of the list elements is:" 
   print (product x) 

我们的代码将产生以下输出:

Our list is:
[1,2,3,4,5]
The multiplication of the list elements is: 
120

Elem 函数

此函数用于检查提供的列表是否包含特定元素。相应地,它返回truefalse

下面的代码检查提供的元素列表是否包含值 786。

main = do 
   let x = [1,45,155,1785] 
   putStrLn "Our list is:" 
   print (x) 
   putStrLn "Does it contain 786?" 
   print (elem 786 (x))

它将产生以下输出:

Our list is:
[1,45,155,1785]
Does it contain 786?
False

使用相同的代码检查提供的列表是否包含值 1785。

Haskell - 函数组合

函数组合是将一个函数的输出用作另一个函数的输入的过程。如果我们学习组合背后的数学原理会更好。在数学中,组合f{g(x)}表示,其中g()是一个函数,其输出用作另一个函数的输入,即f()

函数组合可以使用任何两个函数实现,前提是其中一个函数的输出类型与第二个函数的输入类型匹配。我们在 Haskell 中使用点运算符 (.) 来实现函数组合。

请看下面的示例代码。在这里,我们使用了函数组合来计算输入数字是偶数还是奇数。

eveno :: Int -> Bool 
noto  :: Bool -> String 

eveno x = if x `rem` 2 == 0 
   then True 
else False 
noto x = if x == True 
   then "This is an even Number" 
else "This is an ODD number" 

main = do 
   putStrLn "Example of Haskell Function composition" 
   print ((noto.eveno)(16))

在这里,在函数中,我们同时调用了两个函数,notoeveno。编译器将首先调用函数"eveno()",参数为16。此后,编译器将使用eveno方法的输出作为noto()方法的输入。

其输出如下所示:

Example of Haskell Function composition                
"This is an even Number"

由于我们提供的是数字 16 作为输入(这是一个偶数),eveno()函数返回true,这成为noto()函数的输入并返回输出:“这是一个偶数”。

Haskell - 模块

如果您使用过 Java,那么您就会知道所有类是如何绑定到名为package的文件夹中的。类似地,Haskell 可以被认为是模块的集合。

Haskell 是一种函数式语言,所有内容都表示为表达式,因此模块可以称为类似或相关类型函数的集合。

您可以从一个模块导入一个函数到另一个模块。所有“import”语句都应该在您开始定义其他函数之前出现。在本章中,我们将学习 Haskell 模块的不同特性。

List 模块

List 提供了一些用于处理列表类型数据的出色函数。导入 List 模块后,您可以使用各种各样的函数。

在下面的示例中,我们使用了一些 List 模块中提供的重要的函数。

import Data.List  

main = do  
   putStrLn("Different methods of List Module") 
   print(intersperse '.' "Tutorialspoint.com") 
   print(intercalate " " ["Lets","Start","with","Haskell"]) 
   print(splitAt 7 "HaskellTutorial") 
   print (sort [8,5,3,2,1,6,4,2])

这里,我们有很多函数,甚至没有定义它们。这是因为这些函数在 List 模块中可用。导入 List 模块后,Haskell 编译器使所有这些函数在全局命名空间中可用。因此,我们可以使用这些函数。

我们的代码将产生以下输出:

Different methods of List Module
"T.u.t.o.r.i.a.l.s.p.o.i.n.t...c.o.m"
"Lets Start with Haskell"
("Haskell","Tutorial")
[1,2,2,3,4,5,6,8]

Char 模块

Char 模块有很多预定义函数可用于处理字符类型。请查看以下代码块:

import Data.Char 

main = do  
   putStrLn("Different methods of Char Module") 
   print(toUpper 'a') 
   print(words "Let us study tonight") 
   print(toLower 'A')

这里,函数toUppertoLower 已经在Char 模块中定义。它将产生以下输出:

Different methods of Char Module
'A'
["Let","us","study","tonight"]
'a'

Map 模块

Map 是一种未排序的增值对类型数据类型。它是一个广泛使用的模块,具有许多有用的函数。以下示例显示了如何使用 Map 模块中提供的预定义函数。

import Data.Map (Map) 
import qualified Data.Map as Map  --required for GHCI  

myMap :: Integer -> Map Integer [Integer] 
myMap n = Map.fromList (map makePair [1..n]) 
   where makePair x = (x, [x])  

main = print(myMap 3)

它将产生以下输出:

fromList [(1,[1]),(2,[2]),(3,[3])] 

Set 模块

Set 模块有一些非常有用的预定义函数来操作数学数据。集合实现为二叉树,因此集合中的所有元素都必须唯一。

请查看以下示例代码

import qualified Data.Set as Set   

text1 = "Hey buddy"   
text2 = "This tutorial is for Haskell"   

main = do  
   let set1 = Set.fromList text1   
       set2 = Set.fromList text2 
   print(set1) 
   print(set2)    

这里,我们将字符串修改为集合。它将产生以下输出。注意,输出集合中没有字符重复。

fromList " Hbdeuy"
fromList " HTaefhiklorstu"

自定义模块

让我们看看如何创建一个可以在其他程序中调用的自定义模块。要实现这个自定义模块,我们将创建一个名为"custom.hs" 的单独文件以及我们的"main.hs"

让我们创建自定义模块并在其中定义一些函数。

custom.hs

module Custom ( 
   showEven, 
   showBoolean 
) where 

showEven:: Int-> Bool 
showEven x = do 

if x 'rem' 2 == 0 
   then True 
else False 
showBoolean :: Bool->Int 
showBoolean c = do 

if c == True 
   then 1 
else 0 

我们的自定义模块已准备就绪。现在,让我们将其导入程序。

main.hs

import Custom 

main = do 
   print(showEven 4) 
   print(showBoolean True) 

我们的代码将生成以下输出:

True
1

showEven 函数返回True,因为“4”是一个偶数。showBoolean 函数返回“1”,因为我们传递给函数的布尔函数是“True”。

Haskell - 输入与输出

到目前为止,我们讨论的所有示例本质上都是静态的。在本章中,我们将学习如何与用户动态通信。我们将学习 Haskell 中使用的不同输入和输出技术。

文件和流

到目前为止,我们已经在程序本身中对所有输入进行了硬编码。我们一直在从静态变量获取输入。现在,让我们学习如何从外部文件读取和写入。

让我们创建一个文件并将其命名为“abc.txt”。接下来,在此文本文件中输入以下几行:“欢迎来到 Tutorialspoint。在这里,您将获得学习 Haskell 的最佳资源。”

接下来,我们将编写以下代码,该代码将在控制台上显示此文件的内容。这里,我们使用函数 readFile(),它读取文件直到找到 EOF 字符。

main = do  
   let file = "abc.txt" 
   contents <- readFile file 
   putStrLn contents   

上面的代码段将读取“abc.txt”文件作为一个字符串,直到遇到任何文件结尾字符。这段代码将生成以下输出。

Welcome to Tutorialspoint
Here, you will get the best resource to learn Haskell.

注意,它在终端上打印的内容都写入该文件中。

命令行参数

Haskell 还提供了通过命令提示符操作文件的工具。让我们回到终端并键入“ghci”。然后,键入以下命令集:

let file = "abc.txt" 
writeFile file "I am just experimenting here." 
readFile file 

这里,我们创建了一个名为“abc.txt”的文本文件。接下来,我们使用命令writeFile在文件中插入了一个语句。最后,我们使用命令readFile在控制台上打印文件的内容。我们的代码将产生以下输出:

I am just experimenting here.

异常

异常可以被认为是代码中的错误。在这种情况下,编译器在运行时无法获得预期的输出。与任何其他良好的编程语言一样,Haskell 提供了一种实现异常处理的方法。

如果您熟悉 Java,那么您可能知道 Try-Catch 块,我们通常在其中抛出错误并在catch块中捕获相同的错误。在 Haskell 中,我们也有相同的函数来捕获运行时错误。

try 的函数定义如下所示:“try :: Exception e => IO a -> IO (Either e a)” 请查看以下示例代码。它显示了如何捕获“除以零”异常。

import Control.Exception 

main = do 
   result <- try (evaluate (5 `div` 0)) :: IO (Either SomeException Int) 
   case result of 
      Left ex   -> putStrLn $ "Caught exception: " ++ show ex 
      Right val -> putStrLn $ "The answer was: " ++ show val 

在上面的示例中,我们使用了Control.Exception模块的内置try函数,因此我们提前捕获了异常。上面的代码段将在屏幕上产生以下输出。

Caught exception: divide by zero 

Haskell - Functor (函子)

Haskell 中的Functor是一种对不同类型进行映射的函数式表示。它是实现多态性的高级概念。根据 Haskell 开发人员的说法,所有类型,例如 List、Map、Tree 等,都是 Haskell Functor 的实例。

Functor是一个内置类,其函数定义如下:

class Functor f where 
   fmap :: (a -> b) -> f a -> f b 

根据此定义,我们可以得出结论,Functor是一个函数,它接受一个函数,例如fmap(),并返回另一个函数。在上面的示例中,fmap()是函数map()的广义表示。

在下面的示例中,我们将看到 Haskell Functor 的工作方式。

main = do  
   print(map (subtract 1) [2,4,8,16])      
   print(fmap (subtract 1) [2,4,8,16])   

在这里,我们对列表使用了map()fmap()进行减法运算。您可以观察到这两个语句都将产生相同的包含元素 [1,3,7,15] 的列表结果。

这两个函数都调用了另一个名为subtract()的函数来产生结果。

[1,3,7,15]
[1,3,7,15]

那么,mapfmap有什么区别呢?区别在于它们的用法。Functor使我们能够在不同的数据类型中实现更多函数式方法,例如“just”和“Nothing”。

main = do 
   print (fmap  (+7)(Just 10)) 
   print (fmap  (+7) Nothing)

上面的代码段将在终端上产生以下输出:

Just 17
Nothing

Applicative Functor

Applicative Functor 是一个普通的 Functor,具有 Applicative 类型类提供的额外功能。

使用 Functor,我们通常将现有的函数与其中定义的另一个函数映射。但是,没有办法将 Functor 内部定义的函数与另一个 Functor 映射。这就是为什么我们有另一个名为Applicative Functor的功能。此映射功能由Control模块下定义的 Applicative 类型类实现。此类只给我们提供两种方法:一种是pure,另一种是<*>

以下是 Applicative Functor 的类定义。

class (Functor f) => Applicative f where   
   pure :: a -> f a   
   (<*>) :: f (a -> b) -> f a -> f b   

根据实现,我们可以使用两种方法映射另一个 Functor:“Pure”“<*>”。“Pure”方法应该接受任何类型的值,它将始终返回该值的 Applicative Functor。

以下示例显示了 Applicative Functor 的工作方式:

import Control.Applicative 

f1:: Int -> Int -> Int 
f1 x y = 2*x+y  
main = do  
   print(show $ f1 <$> (Just 1) <*> (Just 2) ) 

在这里,我们在函数f1的函数调用中实现了应用函子。我们的程序将产生以下输出。

"Just 4"

Monoid

我们都知道 Haskell 将所有内容定义为函数的形式。在函数中,我们可以选择将输入作为函数的输出。这就是Monoid

Monoid是一组函数和运算符,其中输出与其输入无关。让我们以函数 (*) 和整数 (1) 为例。现在,无论输入是什么,其输出都将保持相同的数字。也就是说,如果将一个数字乘以 1,您将得到相同的数字。

这是一个 monoid 的类型类定义。

class Monoid m where  
   mempty :: m 
   mappend :: m -> m -> m  
   mconcat :: [m] -> m 
   mconcat = foldr mappend mempty 

请查看以下示例以了解 Monoid 在 Haskell 中的用法。

multi:: Int->Int 
multi x = x * 1 
add :: Int->Int 
add x = x + 0 

main = do  
   print(multi 9)  
   print (add 7)

我们的代码将产生以下输出:

9
7

这里,函数“multi”将输入乘以“1”。类似地,函数“add”将输入与“0”相加。在这两种情况下,输出都将与输入相同。因此,函数{(*),1}{(+),0}是 monoid 的完美示例。

Haskell - Monad (单子)

Monad只不过是一种具有额外功能的 Applicative Functor。它是一个类型类,它遵循三个称为monadic 规则的基本规则。

所有这三个规则都严格适用于 Monad 声明,如下所示:

class Monad m where  
   return :: a -> m a 
   (>>=) :: m a -> (a -> m b) -> m b 
   (>>) :: m a -> m b -> m b 
   x >> y = x >>= \_ -> y 
   fail :: String -> m a  
   fail msg = error msg 

适用于 Monad 声明的三个基本定律是:

  • 左恒等律 - return函数不会更改值,并且它不应该更改 Monad 中的任何内容。它可以表示为“return >>= mf = mf”。

  • 右恒等律 - return函数不会更改值,并且它不应该更改 Monad 中的任何内容。它可以表示为“mf >>= return = mf”。

  • 结合律 - 根据此定律,Functor 和 Monad 实例应该以相同的方式工作。它可以用数学表达式表示为“(f >>= g) >>= h = f >>= (g >>= h)”。

前两个定律迭代相同的点,即return应该在bind运算符的两侧具有恒等行为。

在我们之前的示例中,我们已经使用了许多 Monad,而没有意识到它们是 Monad。考虑以下示例,我们使用 List Monad 来生成特定列表。

main = do
   print([1..10] >>= (\x -> if odd x then [x*2] else []))

此代码将产生以下输出:

[2,6,10,14,18]

Haskell - Zipper (拉链)

Haskell 中的Zipper基本上是指向数据结构(例如)的特定位置的指针。

让我们考虑一个具有 5 个元素[45,7,55,120,56],它可以表示为一个完美的二叉树。如果我想更新此列表的最后一个元素,那么我需要遍历所有元素才能到达最后一个元素才能更新它,对吧?

但是,如果我们可以以这样的方式构建我们的树,即具有N个元素的树是[(N-1),N]的集合。然后,我们不需要遍历所有不需要的(N-1)个元素。我们可以直接更新第 N 个元素。这正是 Zipper 的概念。它专注于或指向树的特定位置,我们可以在其中更新该值而无需遍历整棵树。

在下面的示例中,我们在列表中实现了 Zipper 的概念。同样,可以在文件数据结构中实现 Zipper。

data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)
type Zipper_List a = ([a],[a])    

go_Forward :: Zipper_List a -> Zipper_List a   
go_Forward (x:xs, bs) = (xs, x:bs)   
   
go_Back :: Zipper_List a -> Zipper_List a   
go_Back (xs, b:bs) = (b:xs, bs)    

main = do 
   let list_Ex = [1,2,3,4] 
   print(go_Forward (list_Ex,[]))       
   print(go_Back([4],[3,2,1])) 

编译并执行上述程序时,它将产生以下输出:

([2,3,4],[1]) 
([3,4],[2,1])

这里我们关注的是整个字符串中的一个元素,无论是向前还是向后。

广告