Haskell - 快速指南
Haskell - 概述
Haskell 是一种函数式编程语言,专门用于处理符号计算和列表处理应用程序。函数式编程基于数学函数。除了 Haskell 之外,其他一些遵循函数式编程范式的流行语言包括:Lisp、Python、Erlang、Racket、F#、Clojure 等。
在传统编程中,指令被视为一组特定语法或格式的声明,但在函数式编程中,所有计算都被视为单独数学函数的组合。
使用 Haskell 实现功能化
Haskell 是一种广泛使用的纯函数式语言。在这里,我们列出了使该语言相对于其他传统编程语言(例如 Java、C、C++、PHP 等)如此特殊的几点。
函数式语言- 在传统的编程语言中,我们指示编译器执行一系列任务,这些任务只不过是告诉您的计算机“做什么”和“如何做?” 但在 Haskell 中,我们会告诉计算机“它是什么?”
懒惰- Haskell 是一种懒惰的语言。通过惰性,我们的意思是 Haskell 不会无缘无故地计算任何表达式。当评估引擎发现需要评估某个表达式时,它会创建一个thunk 数据结构来收集该特定评估所需的所有信息以及指向该thunk 数据结构的指针。仅当需要计算特定表达式时,计算引擎才会开始工作。
模块化- Haskell 应用程序只不过是一系列函数。可以说,Haskell 应用程序是众多小型 Haskell 应用程序的集合。
静态类型- 在传统的编程语言中,我们需要定义一系列变量及其类型。相比之下,Haskell 是一种类型干扰语言。通过术语“类型干扰语言”,我们的意思是 Haskell 编译器足够智能,可以找出声明的变量的类型,因此我们不需要显式提及所使用的变量的类型。
可维护性- Haskell 应用程序是模块化的,因此维护它们非常容易且具有成本效益。
函数式程序更加并发,并且在执行时遵循并行性,以提供更准确和更好的性能。哈斯克尔也不例外;它是为了有效处理多线程而开发的。
你好世界
这是一个展示 Haskell 活力的简单示例。看看下面的代码。我们所需要的只是一行在控制台上打印“Hello Word”。
main = putStrLn "Hello World"
一旦 Haskell 编译器遇到上述代码,它会立即产生以下输出 -
Hello World
我们将在本教程中提供大量示例来展示 Haskell 的强大功能和简单性。
Haskell - 环境设置
我们已经在线设置了 Haskell 编程环境 - https://www.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(格拉斯哥 Haskell 编译器)、CABAL(构建应用程序和库的通用架构)和 Stack 工具。
Stack Installer - 在此安装程序中,可以在托管收费链的跨平台中下载 GHC。它将在全球范围内安装您的应用程序,以便在需要时可以更新其 API 工具。它自动解决所有面向 Haskell 的依赖关系。
Haskell Platform - 这是安装 Haskell 的最佳方法,因为它将在您的计算机中安装整个平台,并且从一个特定位置安装。该安装程序不像上面两个安装程序那样是分布式的。
我们已经在市场上看到了不同类型的安装程序,现在让我们看看如何在我们的计算机中使用这些安装程序。在本教程中,我们将使用 Haskell 平台安装程序在我们的系统中安装 Haskell 编译器。
Windows环境搭建
要在 Windows 计算机上设置 Haskell 环境,请访问其官方网站https://www.haskell.org/platform/windows.html并根据您的可定制架构下载安装程序。
检查您的系统架构并下载相应的安装文件并运行它。它将像任何其他 Windows 应用程序一样安装。您可能需要更新系统的 CABAL 配置。
MAC环境搭建
要在 MAC 系统上设置 Haskell 环境,请访问其官方网站https://www.haskell.org/platform/mac.html并下载 Mac 安装程序。
Linux环境搭建
在基于Linux的系统上安装Haskell需要运行一些命令,这不像MAC和Windows那么容易。是的,这很烦人,但很可靠。
您可以按照下面给出的步骤在 Linux 系统上安装 Haskell -
步骤 1 - 要在 Linux 系统上设置 Haskell 环境,请访问官方网站https://www.haskell.org/platform/linux.html并选择您的发行版。您将在浏览器上看到以下屏幕。
步骤 2 - 选择您的发行版。在我们的例子中,我们使用 Ubuntu。选择此选项后,您将在屏幕上看到以下页面,其中包含在本地系统中安装 Haskell 的命令。
步骤 3 - 按 Ctrl + Alt + T 打开终端。运行命令“$ sudo apt-get install haskell-platform”并按 Enter。使用 root 密码对您进行身份验证后,它将自动开始在您的系统上下载 Haskell。安装后,您将收到一条确认消息。
步骤 4 - 再次转到终端并运行 GHCI 命令。一旦出现 Prelude 提示,您就可以在本地系统上使用 Haskell 了。
要退出 GHCI prolog,可以使用命令“:quit exit”。
Haskell - 基本数据模型
Haskell 是一种纯函数式编程语言,因此它比其他编程语言更具交互性和智能性。在本章中,我们将学习 Haskell 的基本数据模型,这些模型实际上是预定义的或以某种方式智能解码到计算机内存中的。
在本教程中,我们将使用我们网站上提供的 Haskell 在线平台 ( https://www.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:不在范围内:`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] 是一个字符列表,因此,根据定义,List 是用逗号分隔的相同数据类型的集合。
与其他数据类型一样,您无需将 List 声明为 List。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'
列表理解
列表理解是使用数学表达式生成列表的过程。看下面的示例,我们使用数学表达式以 [输出 | 格式] 生成一个列表。范围,条件]。
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]
这种使用数学表达式创建列表的方法称为列表理解。
元组
Haskell 提供了另一种在单个数据类型中声明多个值的方法。它被称为元组。元组可以被视为列表,但是元组和列表之间存在一些技术差异。
元组是一种不可变的数据类型,因为我们无法在运行时修改元素的数量,而列表是一种可变的数据类型。
另一方面,List 是同构数据类型,而 Tuple 本质上是异构的,因为 Tuple 内部可能包含不同类型的数据。
元组由单括号表示。看一下下面的示例,了解 Haskell 如何处理元组。
Prelude> (1,1,'a')
它将产生以下输出 -
(1,1,'a')
在上面的示例中,我们使用了一个带有两个数字类型变量和一个字符类型变量的元组。
Haskell - 基本运算符
在本章中,我们将了解 Haskell 中使用的不同运算符。与其他编程语言一样,Haskell 可以智能地处理一些基本运算,如加法、减法、乘法等。在接下来的章节中,我们将详细了解不同的运算符及其用法。
在本章中,我们将通过我们的在线平台(https://www.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)
在上面的文件中,我们创建了两个单独的变量var1和var2。最后,我们使用加法运算符打印结果。使用编译和执行按钮来运行您的代码。
此代码将在屏幕上产生以下输出 -
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)
在此示例中,我们创建了两个变量var1和var2。此后,我们使用减法 (−) 运算符将两个值相减。
此代码将在屏幕上产生以下输出 -
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 的决策结构 -
Haskell 提供以下类型的决策语句 -
先生。 | 声明及说明 |
---|---|
1 | if-else 语句
一个if语句和一个else语句。仅当给定的布尔条件不满足时,else块中的指令才会执行。 |
2 | 嵌套 if-else 语句
多个if块后跟else块 |
Haskell - 类型和类型类
Haskell 是一种函数式语言,并且它是严格类型的,这意味着整个应用程序中使用的数据类型将在编译时为编译器所知。
内置类型类
在 Haskell 中,每个语句都被视为一个数学表达式,该表达式的类别称为Type。您可以说“Type”是编译时使用的表达式的数据类型。
要了解有关Type的更多信息,我们将使用“:t”命令。一般来说,Type可以被认为是一个值,而Type Class可以被认为是一组相似类型的类型。在本章中,我们将了解不同的内置类型。
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可以被视为Int的超集。该值不受任何数字限制,因此整数可以是任意长度,没有任何限制。要了解Int和Integer类型之间的基本区别,让我们将上面的代码修改如下 -
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 类型在 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是末尾带有双精度的浮点数。看一下下面的例子 -
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是布尔类型。它可以是 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 代表字符。单引号内的任何内容都被视为字符。在下面的代码中,我们修改了之前的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 足够智能,可以在声明之前捕获类型。在本教程的后续章节中,我们将看到不同的类型和 Type 类如何使 Haskell 成为强类型语言。
EQ 类型类别
EQ类型类是一个接口,提供测试表达式相等性的功能。任何想要检查表达式相等性的类型类都应该是该 EQ 类型类的一部分。
上面提到的所有标准 Type 类都是该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接口的一部分。与 EQ 接口一样,Ord 接口可以使用“">”、“<”、“<=”、“">=”、“比较”来调用。
请在下面的示例中使用此类型类的“比较”功能。
main = print (4 <= 2)
在这里,Haskell 编译器将检查 4 是否小于或等于 2。由于不小于或等于 2,代码将产生以下输出 -
sh-4.3$ main False
展示
Show具有将其参数打印为字符串的功能。无论它的参数是什么,它总是将结果打印为字符串。在下面的示例中,我们将使用此接口打印整个列表。“show”可以用来调用该接口。
main = print (show [1..10])
它将在控制台上产生以下输出。这里,双引号表示它是一个String类型值。
sh-4.3$ main "[1,2,3,4,5,6,7,8,9,10]"
读
Read接口的作用与Show相同,但它不会以String格式打印结果。在下面的代码中,我们使用read接口读取字符串值并将其转换为 Int 值。
main = print (readInt "12") readInt :: String -> Int readInt = read
在这里,我们将一个 String 变量(“12”)传递给readInt方法,该方法在转换后返回 12(一个 Int 值)。这是它的输出 -
sh-4.3$ main 12
枚举
Enum是 Type 类的另一种类型,它支持 Haskell 中的顺序或有序功能。该 Type 类可以通过Succ、Pred、Bool、Char等命令访问。
以下代码显示如何查找 12 的后继值。
main = print (succ 12)
它将产生以下输出 -
sh-4.3$ main 13
有界
所有具有上限和下限的类型都属于该类型类别。例如,Int类型数据的最大界限为“9223372036854775807”,最小界限为“-9223372036854775808”。
下面的代码展示了 Haskell 如何确定 Int 类型的最大和最小界限。
main = do print (maxBound :: Int) print (minBound :: Int)
它将产生以下输出 -
sh-4.3$ main 9223372036854775807 -9223372036854775808
现在,尝试找出 Char、Float 和 Bool 类型的最大和最小界限。
数量
该类型类用于数字运算。Int、Integer、Float 和 Double 等类型都属于此 Type 类。看一下下面的代码 -
main = do print(2 :: Int) print(2 :: Float)
它将产生以下输出 -
sh-4.3$ main 2 2.0
不可缺少的
Integral可以被认为是 Num Type Class 的子类。Num Type 类保存所有类型的数字,而 Integral 类型类仅用于整数。Int 和 Integer 是该 Type 类下的类型。
漂浮的
与 Integral 一样,Floating 也是 Num Type 类的一部分,但它只保存浮点数。因此,Float和Double属于此类。
自定义类型类
与任何其他编程语言一样,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
模式匹配
模式匹配是匹配特定类型表达式的过程。它只不过是一种简化代码的技术。该技术可以实现到任何类型的 Type 类中。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,但从开发人员的角度来看,Guards更具可读性和简单性。对于初次使用的用户来说,守卫看起来与 If-Else 语句非常相似,但它们在功能上有所不同。
在下面的代码中,我们使用Guard的概念修改了阶乘程序。
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调用事实函数。在内部,编译器将以与模式匹配相同的方式工作,产生以下输出 -
The factorial of 5 is: 120
哪里子句
其中是可在运行时用于生成所需输出的关键字或内置函数。当函数计算变得复杂时,它会非常有帮助。
考虑这样一个场景:您的输入是具有多个参数的复杂表达式。在这种情况下,您可以使用“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的库函数,你会发现大多数库函数都是以高阶方式编写的。
让我们举一个例子,我们将导入一个内置的高阶函数映射,并根据我们的选择使用它来实现另一个高阶函数。
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"
在上面的示例中,我们使用了Type 类Char的toUpper函数将输入转换为大写。在这里,“map”方法将函数作为参数并返回所需的输出。这是它的输出 -
sh-4.3$ ghc -O2 --make *.hs -o main -threaded -rtsopts sh-4.3$ main "TUTORIALSPOINT.COM"
拉姆达表达式
有时我们必须编写一个在应用程序的整个生命周期中仅使用一次的函数。为了处理这种情况,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 中轻松使用的基本函数,而无需导入任何特殊的 Type 类。这些函数中的大多数是其他高阶函数的一部分。
头部功能
Head函数作用于列表。它返回输入参数的第一个,它基本上是一个列表。在下面的示例中,我们传递一个包含 10 个值的列表,并使用head函数生成该列表的第一个元素。
主要=做 让 x = [1..10] putStrLn "我们的列表是:" 打印(x) putStrLn "列表的第一个元素是:" 打印(头x)
它将产生以下输出 -
我们的清单是: [1,2,3,4,5,6,7,8,9,10] 列表的第一个元素是: 1
尾部功能
Tail是对head功能的补充。它接受一个列表作为输入,并生成没有头部的整个列表。这意味着,tail函数返回整个列表,不含第一个元素。看一下下面的例子 -
主要=做 让 x = [1..10] putStrLn "我们的列表是:" 打印(x) putStrLn "我们列表的尾部是:" 打印(尾部x)
它将产生以下输出 -
我们的清单是: [1,2,3,4,5,6,7,8,9,10] 我们列表的末尾是: [2,3,4,5,6,7,8,9,10]
最后一个功能
顾名思义,它生成作为输入提供的列表的最后一个元素。检查以下示例。
主要=做 让 x = [1..10] putStrLn "我们的列表是:" 打印(x) putStrLn“我们列表的最后一个元素是:” 打印(最后一个x)
它将产生以下输出 -
我们的清单是: [1,2,3,4,5,6,7,8,9,10] 我们列表的最后一个元素是: 10
初始化函数
Init 的工作原理与tail函数完全相反。它接受一个列表作为参数,并返回整个列表,不包含最后一个条目。
主要=做 让 x = [1..10] putStrLn "我们的列表是:" 打印(x) putStrLn "我们的列表没有最后一个条目:" 打印(初始化x)
现在,观察它的输出 -
我们的清单是: [1,2,3,4,5,6,7,8,9,10] 我们的列表没有最后一个条目: [1,2,3,4,5,6,7,8,9]
空函数
Null是一个布尔检查函数,适用于 String 并仅当给定列表为空时返回True,否则返回False。以下代码检查提供的列表是否为空。
主要=做 让 x = [1..10] putStrLn "我们的列表是:" 打印(x) putStrLn“我们的列表是空的吗?” 打印(空x)
它将产生以下输出 -
我们的清单是: [1,2,3,4,5,6,7,8,9,10] 我们的列表是空的吗? 错误的
反转功能
它作用于字符串输入,并将整个输入转换为相反的顺序,并给出一个输出作为结果。以下是该函数的代码库。
主要=做 让 x = [1..10] putStrLn "我们的列表是:" 打印(x) putStrLn "倒序排列的列表是:" 打印(反转x)
它将产生以下输出 -
我们的清单是: [1,2,3,4,5,6,7,8,9,10] 倒序排列的列表是: [10,9,8,7,6,5,4,3,2,1]
长度函数
该函数用于计算作为参数给出的列表的长度。看一下下面的例子 -
主要=做 让 x = [1..10] putStrLn "我们的列表是:" 打印(x) putStrLn "此列表的长度是:" 打印(长度x)
我们的列表中有 10 个元素,因此我们的代码将产生 10 作为输出。
我们的清单是: [1,2,3,4,5,6,7,8,9,10] 该列表的长度为: 10
采取功能
Take函数用于从另一个字符串创建子字符串。以下代码展示了如何在 Haskell 中使用 take 函数 -
main = print(取 5 ([1 .. 10]))
该代码生成一个子字符串,其中包含所提供列表中的 5 个元素 -
[1,2,3,4,5]
下降功能
该函数还用于生成子字符串。它的功能与take功能相反。看下面的代码 -
主要 = 打印(滴 5 ([1 .. 10]))
该代码从提供的列表中删除前 5 个元素并打印其余 5 个元素。它将产生以下输出 -
[6,7,8,9,10]
最大功能
该函数用于从提供的列表中查找具有最大值的元素。让我们看看如何在实践中使用它 -
主要=做 让 x = [1,45,565,1245,02,2] putStrLn "列表的最大值元素是:" 打印(最大x)
上面的代码将生成以下输出 -
列表的最大值元素为: 1245
最小函数
该函数用于从提供的列表中查找具有最小值的元素。它正好与极大值函数相反。
主要=做 让 x = [1,45,565,1245,02,2] putStrLn "列表的最小值元素是:" 打印(最小x)
上述代码的输出是 -
列表的最小值元素为: 1
求和函数
顾名思义,该函数返回所提供列表中所有元素的总和。以下代码采用 5 个元素的列表,并返回它们的总和作为输出。
主要=做 让 x = [1..5] putStrLn "我们的列表是:" 打印(x) putStrLn "列表元素的总和是:" 打印(总和x)
它将产生以下输出 -
我们的清单是: [1,2,3,4,5] 列表元素的总和为: 15
产品功能
您可以使用此函数将列表中的所有元素相乘并打印其值。
主要=做 让 x = [1..5] putStrLn "我们的列表是:" 打印(x) putStrLn "列表元素的乘法是:" 打印(产品x)
我们的代码将产生以下输出 -
我们的清单是: [1,2,3,4,5] 列表元素的乘法为: 120
元素函数
此函数用于检查提供的列表是否包含特定元素。因此,它返回true或false。
以下代码检查提供的元素列表是否包含值 786。
主要=做 让 x = [1,45,155,1785] putStrLn "我们的列表是:" 打印(x) putStrLn "它包含 786 吗?" 打印(元素786(x))
它将产生以下输出 -
我们的清单是: [1,45,155,1785] 里面有786吗? 错误的
使用相同的代码检查提供的列表是否包含值 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))
在这里,在main函数中,我们同时调用两个函数noto和Eveno。编译器将首先以16作为参数调用函数“eveno()” 。此后,编译器将使用eveno方法的输出作为noto()方法的输入。
其输出如下 -
Example of Haskell Function composition "This is an even Number"
由于我们提供数字 16 作为输入(这是一个偶数),eveno()函数返回true ,它成为noto()函数的输入并返回输出:“这是一个偶数”。
Haskell - 模块
如果您使用过 Java,那么您就会知道所有类是如何绑定到名为package的文件夹中的。同样,Haskell 可以被认为是模块的集合。
Haskell 是一种函数式语言,所有内容都表示为表达式,因此模块可以被称为相似或相关类型函数的集合。
您可以将一个函数从一个模块导入到另一个模块中。在开始定义其他函数之前,所有“导入”语句都应该首先出现。在本章中,我们将学习 Haskell 模块的不同功能。
列表模块
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模块有大量预定义函数可用于处理字符类型。看一下下面的代码块 -
import Data.Char main = do putStrLn("Different methods of Char Module") print(toUpper 'a') print(words "Let us study tonight") print(toLower 'A')
此处,函数toUpper和toLower已在Char模块内定义。它将产生以下输出 -
Different methods of Char Module 'A' ["Let","us","study","tonight"] 'a'
地图模块
Map是一种未排序的增值pair型数据类型。它是一个广泛使用的模块,具有许多有用的功能。以下示例显示如何使用地图模块中提供的预定义函数。
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 模块有一些非常有用的预定义函数来操作数学数据。集合被实现为二叉树,因此集合中的所有元素必须是唯一的。
看看下面的示例代码
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”。
让我们创建自定义模块并在其中定义一些函数。
定制.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
我们的自定义模块已准备就绪。现在,让我们将其导入到程序中。
主要.hs
import Custom main = do print(showEven 4) print(showBoolean True)
我们的代码将生成以下输出 -
True 1
showEven函数返回True ,因为“ 4”是偶数。showBool ean函数返回“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 提供了一种实现 excepti 的方法