Elixir - 快速指南


Elixir - 概述

Elixir 是一种动态的函数式语言,旨在构建可扩展且可维护的应用程序。它利用 Erlang VM,该 VM 以运行低延迟、分布式和容错系统而闻名,同时也成功用于 Web 开发和嵌入式软件领域。

Elixir 是一种构建在 Erlang 和 Erlang VM 之上的函数式动态语言。Erlang 是一种语言,最初由爱立信于 1986 年编写,旨在帮助解决分布、容错和并发等电话问题。Elixir 由 José Valim 编写,扩展了 Erlang 并为 Erlang VM 提供了更友好的语法。它在做到这一点的同时保持了与 Erlang 相同水平的性能。

灵丹妙药的特点

现在让我们讨论 Elixir 的一些重要特性 -

  • 可扩展性- 所有 Elixir 代码都在隔离的轻量级进程内运行,并通过消息交换信息。

  • 容错- Elixir 提供了管理程序,描述了当出现问题时如何重新启动系统的某些部分,回到保证工作的已知初始状态。这可确保您的应用程序/平台永远不会宕机。

  • 函数式编程- 函数式编程提倡一种编码风格,帮助开发人员编写简短、快速且可维护的代码。

  • 构建工具- Elixir 附带了一组开发工具。Mix 就是这样一种工具,可以轻松创建项目、管理任务、运行测试等。它还有自己的包管理器 - Hex。

  • Erlang 兼容性- Elixir 在 Erlang VM 上运行,使开发人员可以完全访问 Erlang 的生态系统。

Elixir - 环境

为了运行 Elixir,您需要在系统上进行本地设置。

要安装 Elixir,您首先需要 Erlang。在某些平台上,Elixir 软件包附带了 Erlang。

安装 Elixir

现在让我们了解 Elixir 在不同操作系统中的安装。

Windows 设置

要在 Windows 上安装 Elixir,请从https://repo.hex.pm/elixirwebsetup.exe下载安装程序,然后单击“下一步”继续完成所有步骤。您将在本地系统上找到它。

如果您在安装时遇到任何问题,可以查看此页面以获取更多信息。

Mac 设置

如果您安装了 Homebrew,请确保它是最新版本。要更新,请使用以下命令 -

brew update

现在,使用下面给出的命令安装 Elixir -

brew install elixir

Ubuntu/Debian 设置

在 Ubuntu/Debian 设置中安装 Elixir 的步骤如下 -

添加 Erlang 解决方案存储库 -

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo 
dpkg -i erlang-solutions_1.0_all.deb 
sudo apt-get update 

安装 Erlang/OTP 平台及其所有应用程序 -

sudo apt-get install esl-erlang 

安装 Elixir -

sudo apt-get install elixir

其他 Linux 发行版

如果您有任何其他 Linux 发行版,请访问此页面在您的本地系统上设置 Elixir。

测试设置

要测试系统上的 Elixir 设置,请打开终端并在其中输入 iex。它将打开交互式 Elixir shell,如下所示 -

Erlang/OTP 19 [erts-8.0] [source-6dc93c1] [64-bit] 
[smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]  

Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help) 
iex(1)>

Elixir 现在已在您的系统上成功设置。

Elixir - 基本语法

我们将从习惯的“Hello World”程序开始。

要启动 Elixir 交互式 shell,请输入以下命令。

iex

shell 启动后,使用IO.puts函数将字符串“放置”到控制台输出上。在 Elixir shell 中输入以下内容 -

IO.puts "Hello world"

在本教程中,我们将使用 Elixir 脚本模式,将 Elixir 代码保存在扩展名为.ex的文件中。现在让我们将上述代码保留在test.ex文件中。在接下来的步骤中,我们将使用elixirc执行它-

IO.puts "Hello world"

现在让我们尝试运行上面的程序,如下所示 -

$elixirc test.ex

上述程序生成以下结果 -

Hello World

这里我们调用函数IO.puts来生成一个字符串作为输出到我们的控制台。这个函数也可以像我们在 C、C++、Java 等中那样调用,在函数名称后面的括号中提供参数 -

IO.puts("Hello world") 

评论

单行注释以“#”符号开头。没有多行注释,但您可以堆叠多个注释。例如 -

#This is a comment in Elixir

行结尾

不需要像“;”这样的行结尾 在长生不老药中。但是,我们可以使用“;”在同一行中包含多个语句。例如,

IO.puts("Hello"); IO.puts("World!")

上述程序生成以下结果 -

Hello 
World!

身份标识

变量、函数名等标识符用于标识变量、函数等。在 Elixir 中,您可以以小写字母开头,然后是数字、下划线和大写字母来命名标识符。这种命名约定通常称为“snake_case”。例如,以下是 Elixir 中的一些有效标识符 -

var1       variable_2      one_M0r3_variable

请注意,变量也可以用前导下划线命名。不打算使用的值必须分配给 _ 或以下划线开头的变量 -

_some_random_value = 42

Elixir 还依赖下划线将函数设为模块私有。如果您在模块中以前导下划线命名函数并导入该模块,则不会导入该函数。

Elixir 中还有许多与函数命名相关的复杂问题,我们将在接下来的章节中讨论。

保留字

以下字是保留字,不能用作变量、模块或函数名称。

after     and     catch     do     inbits     inlist     nil     else     end 
not     or     false     fn     in     rescue     true     when     xor 
__MODULE__    __FILE__    __DIR__    __ENV__    __CALLER__ 

Elixir - 数据类型

对于使用任何语言,您都需要了解该语言支持的基本数据类型。在本章中,我们将讨论 Elixir 语言支持的 7 种基本数据类型:整数、浮点数、布尔值、Atomics、字符串、列表和元组。

数值类型

Elixir 与任何其他编程语言一样,支持整数和浮点数。如果您打开 Elixir shell 并输入任何整数或浮点数作为输入,它将返回其值。例如,

42

当上面的程序运行时,它会产生以下结果 -

42

您还可以定义八进制、十六进制和二进制的数字。

八进制

要定义八进制数,请在其前面加上“0o”前缀。例如,八进制的 0o52 相当于十进制的 42。

十六进制

要定义十进制数,请在其前面加上“0x”前缀。例如,十六进制的 0xF1 相当于十进制的 241。

二进制

要定义二进制数,请在其前面加上“0b”前缀。例如,二进制的 0b1101 相当于十进制的 13。

Elixir 支持 64 位双精度浮点数。它们也可以使用求幂风格来定义。例如,10145230000可以写成1.014523e10

Atomics

Atomics是常量,其名称就是它们的值。它们可以使用 color(:) 符号创建。例如,

:hello

布尔值

Elixir 支持布尔值truefalse 。这两个值实际上分别附加到Atomics :true 和 :false 上。

弦乐

Elixir 中的字符串插入在双引号之间,并以 UTF-8 编码。它们可以跨越多行并包含插值。要定义字符串,只需将其放在双引号中 -

"Hello world"

为了定义多行字符串,我们使用类似于 python 的语法,带有三个双引号 -

"""
Hello
World!
"""

我们将在字符串章节中深入了解字符串、二进制文件和字符列表(类似于字符串)。

二进制文件

二进制文件是包含在 << >> 中并用逗号分隔的字节序列。例如,

<< 65, 68, 75>>

二进制文件主要用于处理位和字节相关的数据(如果有的话)。默认情况下,它们可以在每个值中存储 0 到 255。可以通过使用大小函数来增加此大小限制,该函数表示存储该值需要多少位。例如,

<<65, 255, 289::size(15)>>

列表

Elixir 使用方括号来指定值列表。值可以是任何类型。例如,

[1, "Hello", :an_atom, true]

列表带有用于列表头和尾部的内置函数,名为 hd 和 tl,它们分别返回列表的头和尾部。有时,当您创建列表时,它会返回一个字符列表。这是因为当 elixir 看到可打印 ASCII 字符列表时,它会将其打印为字符列表。请注意,字符串和字符列表不相等。我们将在后面的章节中进一步讨论列表。

元组

Elixir 使用大括号来定义元组。与列表一样,元组可以保存任何值。

{ 1, "Hello", :an_atom, true 

这里出现了一个问题:当列表元组的工作方式相同时,为什么要同时提供它们呢?那么他们有不同的实现。

  • 列表实际上是以链表的形式存储的,因此列表中的插入、删除速度非常快。

  • 另一方面,元组存储在连续的内存块中,这使得访问它们更快,但会增加插入和删除的额外成本。

Elixir - 变量

变量为我们提供了程序可以操作的命名存储。Elixir 中的每个变量都有一个特定的类型,它决定了变量内存的大小和布局;该内存中可以存储的值的范围;以及可以应用于变量的操作集。

变量的类型

Elixir 支持以下基本类型的变量。

整数

这些用于整数。它们的大小在 32 位架构上为 32 位,在 64 位架构上为 64 位。整数在 Elixir 中总是有符号的。如果一个整数的大小开始超出其限制,elixir 会将其转换为一个大整数,该整数会占用 3 到 n 个字的内存,以能够容纳它的内存为准。

花车

Elixir 中的浮点数具有 64 位精度。就内存而言,它们也类似于整数。定义浮点数时,可以使用指数表示法。

布尔值

它们可以采用 2 个值,即 true 或 false。

弦乐

Elixir 中的字符串采用 utf-8 编码。他们有一个字符串模块,为程序员提供了很多操作字符串的功能。

匿名函数/Lambda

这些是可以定义并分配给变量的函数,然后可以使用该变量来调用该函数。

收藏

Elixir 中有很多可用的集合类型。其中一些是列表、元组、映射、二进制等。这些将在后续章节中讨论。

变量声明

变量声明告诉解释器在哪里以及为变量创建存储空间的大小。Elixir 不允许我们只声明一个变量。变量必须同时声明和赋值。例如,要创建一个名为 life 的变量并为其分配值 42,我们执行以下操作 -

life = 42

这会将变量 life绑定到值 42。如果我们想为该变量重新分配一个新值,我们可以使用与上面相同的语法来完成此操作,即:

life = "Hello world"

变量命名

Elixir 中的命名变量遵循Snake_case约定,即所有变量必须以小写字母开头,后跟 0 个或多个字母(大写和小写),最后跟一个可选的“?” 或者 '!'。

变量名也可以以前导下划线开头,但只有在忽略该变量时才必须使用该下划线,即该变量将不会再次使用,但需要分配给某些内容。

打印变量

在交互式 shell 中,如果您只需输入变量名称,就会打印变量。例如,如果您创建一个变量 -

life = 42 

在 shell 中输入“life”,您将得到如下输出:

42

但是,如果您想将变量输出到控制台(从文件运行外部脚本时),您需要提供变量作为IO.puts函数的输入 -

life = 42  
IO.puts life 

或者

life = 42 
IO.puts(life) 

这将为您提供以下输出 -

42

Elixir - 操作员

运算符是告诉编译器执行特定数学或逻辑操作的符号。Elixir 提供了很多运算符。它们分为以下几类 -

  • 算术运算符
  • 比较运算符
  • 布尔运算符
  • 杂项运算符

算术运算符

下表列出了 Elixir 语言支持的所有算术运算符。假设变量A为 10,变量B为 20,则 -

显示示例

操作员 描述 例子
+ 添加 2 个数字。 A + B 将为 30
- 从第一个数字中减去第二个数字。 AB 会给出-10
* 两个数字相乘。 A*B 会给 200
/ 将第一个数字除以第二个数字。这会将数字转换为浮点数并给出浮点数结果 A/B 给出 0.5。
分区 该函数用于获取除法的商。 div(10,20) 将给出 0
雷姆 该函数用于获取除法的余数。 rem(A, B) 将给出 10

比较运算符

Elixir 中的比较运算符与大多数其他语言中提供的比较运算符大多相同。下表总结了 Elixir 中的比较运算符。假设变量A为 10,变量B为 20,则 -

显示示例

操作员 描述 例子
== 检查左侧的值是否等于右侧的值(如果类型不同,则类型转换值)。 A == B 将给出 false
!= 检查左侧的值是否不等于右侧的值。 A != B 将给出 true
=== 检查左侧值的类型是否等于右侧值的类型,如果是,则检查相同的值。 A === B 会给出 false
!== 与上面相同,但检查不平等而不是平等。 A !== B 将给出 true
> 检查左操作数的值是否大于右操作数的值;如果是,则条件成立。 A > B 将给出 false
< 检查左操作数的值是否小于右操作数的值;如果是,则条件成立。 A < B 将给出 true
>= 检查左操作数的值是否大于或等于右操作数的值;如果是,则条件成立。 A >= B 将给出 false
<= 检查左操作数的值是否小于或等于右操作数的值;如果是,则条件成立。 A <= B 将给出 true

逻辑运算符

Elixir 提供 6 个逻辑运算符:and、or、not、&&、|| 和 !。前三个and or not是严格的布尔运算符,这意味着它们希望第一个参数是布尔值。非布尔参数将引发错误。接下来的三个,&&,|| 和 !不严格,不要求我们将第一个值严格作为布尔值。他们的工作方式与严格的同行相同。假设变量A为真且变量B为 20,则 -

显示示例

操作员 描述 例子
检查提供的两个值是否为真,如果是,则返回第二个变量的值。(逻辑与)。 A和B各给20
或者 检查提供的任一值是否为真。返回真实值。否则返回 false。(逻辑或)。 A 或 B 将给出 true
不是 一元运算符反转给定输入的值。 不 A 会给出 false
&& 非严格的. 与 and工作相同,但不期望第一个参数是布尔值。 B&&A 会给 20
|| 非严格. 与or作用相同,但不期望第一个参数是布尔值。 乙|| A将给出真实的
非严格工作方式与not相同,但不期望参数是布尔值。 !A 会给出 false

注意 -&&|| || 是短路操作员。这意味着如果and的第一个参数为 false,则它将不会进一步检查第二个参数。如果or的第一个参数为 true,则它不会检查第二个参数。例如,

false and raise("An error")  
#This won't raise an error as raise function wont get executed because of short
#circuiting nature of and operator

按位运算符

位运算符作用于位并执行逐位运算。Elixir 提供了按位模块作为Bitwise包的一部分,因此为了使用这些模块,您需要使用按位模块。要使用它,请在 shell 中输入以下命令 -

use Bitwise

对于以下示例,假设 A 为 5,B 为 6 -

显示示例

操作员 描述 例子
&&& 如果两个操作数中都存在,则按位与运算符会复制一个位到结果中。 A &&& B 将给出 4
||| 按位或运算符复制一个位到结果(如果它存在于任一操作数中)。 一个||| B 给 7
>>> 按位右移运算符将第一个操作数位向右移动第二个操作数中指定的数字。 A >>> B 将给出 0
<<< 按位左移运算符将第一个操作数位向左移动第二个操作数中指定的数字。 A <<< B 将给出 320
^^^ 仅当两个操作数上的位不同时,按位异或运算符才会复制一个位到结果。 A ^^^ B 给出 3
~~~ 一元按位不反转给定数字的位。 ~~~A将给出-6

杂项运算符

除了上述运算符之外,Elixir 还提供了一系列其他运算符,例如串联运算符、匹配运算符、Pin 运算符、管道运算符、字符串匹配运算符、代码点运算符、捕获运算符、三元运算符,使其成为一门非常强大的语言。

显示示例

Elixir - 模式匹配

模式匹配是 Elixir 继承自 Erlang 的一项技术。这是一种非常强大的技术,允许我们从复杂的数据结构(如列表、元组、映射等)中提取更简单的子结构。

一场比赛有两个主要部分,左侧右侧。右侧是任何类型的数据结构。左侧尝试匹配右侧的数据结构,并将左侧的任何变量绑定到右侧的相应子结构。如果未找到匹配项,操作员将引发错误。

最简单的匹配是左侧的单独变量和右侧的任何数据结构。该变量将匹配任何内容。例如,

x = 12
x = "Hello"
IO.puts(x)

您可以将变量放置在结构内,以便捕获子结构。例如,

[var_1, _unused_var, var_2] = [{"First variable"}, 25, "Second variable" ]
IO.puts(var_1)
IO.puts(var_2)

这将在var_1中存储值{"First variable"},在var_2中存储 "Second variable"。还有一个特殊的_变量(或以 '_' 为前缀的变量),其工作方式与其他变量完全相同,但告诉 elixir,“确保这里有东西,但我不关心它到底是什么。” 。在前面的示例中,_unused_var就是这样一个变量。

我们可以使用这种技术来匹配更复杂的模式。例如如果您想解开并获取元组中的一个数字,该数字位于列表中,而列表本身位于列表中,则可以使用以下命令 -

[_, [_, {a}]] = ["Random string", [:an_atom, {24}]]
IO.puts(a)

上述程序生成以下结果 -

24

这会将a绑定到 24。当我们使用“_”时,其他值将被忽略。

在模式匹配中,如果我们使用右侧的变量,则使用它的值。如果您想使用左侧变量的值,则需要使用 pin 运算符。

例如,如果您有一个值为 25 的变量“a”,并且您想将其与另一个值为 25 的变量“b”匹配,那么您需要输入 -

a = 25
b = 25
^a = b

最后一行匹配a的当前值,而不是将其分配给b的值。如果我们有一组不匹配的左侧和右侧,则匹配运算符会引发错误。例如,如果我们尝试将一个元组与一个列表进行匹配,或者将一个大小为 2 的列表与一个大小为 3 的列表进行匹配,则会显示错误。

Elixir - 决策

决策结构要求程序员指定一个或多个要由程序评估或测试的条件,以及在条件确定为 true 时要执行的一条或多条语句,以及可选的在条件确定时要执行的其他语句确定是假的

以下是大多数编程语言中典型决策结构的一般形式 -

决策

Elixir 与许多其他编程语言一样提供 if/else 条件结构。它还有一个cond语句,调用它找到的第一个真值。Case 是另一种控制流语句,它使用模式匹配来控制程序的流程。让我们深入了解一下它们。

Elixir 提供以下类型的决策语句。单击以下链接查看其详细信息。

先生。 声明及说明
1 if 语句

if 语句由一个布尔表达式、后跟do、一个或多个可执行语句以及最后一个end关键字组成。仅当布尔条件计算结果为 true 时,if 语句中的代码才会执行​​。

2 if..else 语句

if 语句后面可以跟一个可选的 else 语句(在 do..end 块内),该语句在布尔表达式为 false 时执行。

3 除非声明

except 语句与 if 语句具有相同的主体。except 语句中的代码仅当指定的条件为 false 时才执行。

4 除非..else语句

except..else 语句与 if..else 语句具有相同的主体。except 语句中的代码仅当指定的条件为 false 时才执行。

5 条件

cond 语句用于我们想要根据多个条件执行代码的地方。它的工作原理类似于其他几种编程语言中的 if...else if....else 结构。

6 案件

case 语句可以被认为是命令式语言中 switch 语句的替代品。Case 接受一个变量/文字,并在不同的情况下对其应用模式匹配。如果任何 case 匹配,Elixir 就会执行与该 case 相关的代码并退出 case 语句。

Elixir - 弦乐

Elixir 中的字符串插入在双引号之间,并以 UTF-8 编码。与默认字符串采用 ASCII 编码且只能包含 256 个不同字符的 C 和 C++ 不同,UTF-8 由 1,112,064 个代码点组成。这意味着 UTF-8 编码由许多不同的可能字符组成。由于字符串使用 utf-8,因此我们还可以使用以下符号:ö、ł 等。

创建一个字符串

要创建字符串变量,只需将字符串分配给变量 -

str = "Hello world"

要将其打印到控制台,只需调用IO.puts函数并向其传递变量 str -

str = str = "Hello world" 
IO.puts(str)

上述程序生成以下结果 -

Hello World

空字符串

您可以使用字符串文字""创建一个空字符串。例如,

a = ""
if String.length(a) === 0 do
   IO.puts("a is an empty string")
end

上述程序生成以下结果。

a is an empty string

字符串插值

字符串插值是一种通过将常量、变量、文字和表达式的值包含在字符串文字中来构造新字符串值的方法。Elixir 支持字符串插值,要在字符串中使用变量,在编写字符串时,用大括号括起来,并在大括号前面加上“#”符号。

例如,

x = "Apocalypse" 
y = "X-men #{x}"
IO.puts(y)

这将取 x 的值并将其替换为 y。上面的代码将生成以下结果 -

X-men Apocalypse

字符串连接

我们已经在前面的章节中看到了字符串连接的使用。'<>' 运算符用于连接 Elixir 中的字符串。要连接 2 个字符串,

x = "Dark"
y = "Knight"
z = x <> " " <> y
IO.puts(z)

上面的代码生成以下结果 -

Dark Knight

字符串长度

要获取字符串的长度,我们使用String.length函数。将字符串作为参数传递,它会显示它的大小。例如,

IO.puts(String.length("Hello"))

运行上面的程序时,它会产生以下结果 -

5

反转字符串

要反转字符串,请将其传递给 String.reverse 函数。例如,

IO.puts(String.reverse("Elixir"))

上述程序生成以下结果 -

rixilE

字符串比较

要比较 2 个字符串,我们可以使用 == 或 === 运算符。例如,

var_1 = "Hello world"
var_2 = "Hello Elixir"
if var_1 === var_2 do
   IO.puts("#{var_1} and #{var_2} are the same")
else
   IO.puts("#{var_1} and #{var_2} are not the same")
end

上述程序生成以下结果 -

Hello world and Hello elixir are not the same.

字符串匹配

我们已经看到了 =~ 字符串匹配运算符的使用。要检查字符串是否与正则表达式匹配,我们还可以使用字符串匹配运算符或 String.match? 功能。例如,

IO.puts(String.match?("foo", ~r/foo/))
IO.puts(String.match?("bar", ~r/foo/))

上述程序生成以下结果 -

true 
false

这同样可以通过使用 =~ 运算符来实现。例如,

IO.puts("foo" =~ ~r/foo/)

上述程序生成以下结果 -

true

字符串函数

Elixir 支持大量与字符串相关的函数,下表列出了一些最常用的函数。

先生。 功能及其目的
1

在(字符串,位置)

返回给定 utf8 字符串位置的字素。如果位置大于字符串长度,则返回 nil

2

大写(字符串)

将给定字符串中的第一个字符转换为大写,其余字符转换为小写

3

包含?(字符串,内容)

检查字符串是否包含任何给定内容

4

小写(字符串)

将给定字符串中的所有字符转换为小写

5

结尾为?(字符串,后缀)

如果字符串以给定的任何后缀结尾,则返回 true

6

第一个(字符串)

返回 utf8 字符串中的第一个字素,如果字符串为空则返回 nil

7

最后(字符串)

返回 utf8 字符串的最后一个字素,如果字符串为空则返回 nil

8

替换(主题、模式、替换、选项 \\ [])

返回通过用替换替换主题中出现的模式而创建的新字符串

9

切片(字符串,开始,长度)

返回从偏移量 start 开始、长度为 len 的子字符串

10

分割(字符串)

在每个 Unicode 空格出现处将字符串划分为子字符串,并忽略前导和尾随空格。空白组被视为单个出现。不间断空白处不会发生除法

11

大写(字符串)

将给定字符串中的所有字符转换为大写

二进制文件

二进制只是一个字节序列。二进制文件使用<< >>定义。例如:

<< 0, 1, 2, 3 >>

当然,这些字节可以以任何方式组织,即使按照不能使它们成为有效字符串的顺序也是如此。例如,

<< 239, 191, 191 >>

字符串也是二进制的。而字符串连接运算符<>实际上是一个二进制连接运算符:

IO.puts(<< 0, 1 >> <> << 2, 3 >>)

上面的代码生成以下结果 -

<< 0, 1, 2, 3 >>

请注意 ł 字符。由于这是 utf-8 编码的,因此该字符表示占用 2 个字节。

由于二进制中表示的每个数字都是一个字节,因此当该值从 255 增加时,它会被截断。为了防止这种情况,我们使用大小修饰符来指定我们希望该数字占用多少位。例如 -

IO.puts(<< 256 >>) # truncated, it'll print << 0 >>
IO.puts(<< 256 :: size(16) >>) #Takes 16 bits/2 bytes, will print << 1, 0 >>

上述程序将生成以下结果 -

<< 0 >>
<< 1, 0 >>

我们还可以使用 utf8 修饰符,如果一个字符是代码点,那么它将在输出中产生;否则字节 -

IO.puts(<< 256 :: utf8 >>)

上述程序生成以下结果 -

Ā

我们还有一个名为is_binary的函数,用于检查给定变量是否为二进制。请注意,只有存储为 8 位倍数的变量才是二进制。

位串

如果我们使用大小修饰符定义一个二进制文件并传递给它一个不是 8 倍数的值,我们最终会得到一个位串而不是二进制文件。例如,

bs = << 1 :: size(1) >>
IO.puts(bs)
IO.puts(is_binary(bs))
IO.puts(is_bitstring(bs))

上述程序生成以下结果 -

<< 1::size(1) >>
false
true

这意味着变量bs不是二进制而是位串。我们也可以说二进制是一个位串,其中位数可以被 8 整除。模式匹配对二进制和位串的工作方式相同。

Elixir - 角色列表

字符列表只不过是字符列表。考虑下面的程序来理解同样的内容。

IO.puts('Hello')
IO.puts(is_list('Hello'))

上述程序生成以下结果 -

Hello
true

字符列表不包含字节,而是包含单引号之间的字符的代码点。因此,双引号表示字符串(即二进制),而单引号表示字符列表(即列表)。请注意,如果任何字符超出 ASCII 范围,IEx 将仅生成代码点作为输出。

字符列表主要在与 Erlang 交互时使用,特别是不接受二进制文件作为参数的旧库。您可以使用 to_string(char_list) 和to_char_list(string)函数将字符列表转换为字符串并返回-

IO.puts(is_list(to_char_list("hełło")))
IO.puts(is_binary(to_string ('hełło')))

上述程序生成以下结果 -

true
true

注意- 函数to_stringto_char_list是多态的,即它们可以接受多种类型的输入,如Atomics、整数,并将它们分别转换为字符串和字符列表。

Elixir - 列表和元组

(链接)列表

链表是存储在内存中不同位置并通过引用进行跟踪的元素的异构列表。链表是特别用于函数式编程的数据结构。

Elixir 使用方括号来指定值列表。值可以是任何类型 -

[1, 2, true, 3]

当 Elixir 看到可打印 ASCII 数字列表时,Elixir 会将其打印为字符列表(字面意思是字符列表)。每当您在 IEx 中看到一个值并且不确定它是什么时,您可以使用i函数来检索有关它的信息。

IO.puts([104, 101, 108, 108, 111])

列表中的上述字符都是可打印的。当上面的程序运行时,它会产生以下结果 -

hello

您还可以使用单引号以相反的方式定义列表 -

IO.puts(is_list('Hello'))

当上面的程序运行时,它会产生以下结果 -

true

请记住,单引号和双引号表示在 Elixir 中并不等效,因为它们由不同的类型表示。

列表的长度

为了找到列表的长度,我们使用长度函数,如以下程序所示 -

IO.puts(length([1, 2, :true, "str"]))

上述程序生成以下结果 -

4

连接和减法

可以使用++--运算符连接和减去两个列表。请考虑以下示例以了解这些功能。

IO.puts([1, 2, 3] ++ [4, 5, 6])
IO.puts([1, true, 2, false, 3, true] -- [true, false])

这将在第一种情况下为您提供连接字符串,在第二种情况下为您提供减去的字符串。上述程序生成以下结果 -

[1, 2, 3, 4, 5, 6]
[1, 2, 3, true]

列表的头和尾

头是列表的第一个元素,尾是列表的其余元素。可以使用函数hdtl检索它们。让我们将一个列表分配给一个变量并检索它的头和尾。

list = [1, 2, 3]
IO.puts(hd(list))
IO.puts(tl(list))

这将为我们提供列表的头部和尾部作为输出。上述程序生成以下结果 -

1
[2, 3]

注意- 获取空列表的头部或尾部是错误的。

其他列表功能

Elixir 标准库提供了大量处理列表的函数。我们将在这里看看其中的一些。您可以在此处查看其余列表

S.号 函数名称和描述
1

删除(列表,项目)

从列表中删除给定的项目。返回没有该项目的列表。如果该项目在列表中出现多次,则仅删除第一次出现的项目。

2

delete_at(列表,索引)

通过删除指定索引处的值来生成新列表。负索引表示距列表末尾的偏移量。如果索引越界,则返回原始列表。

3

第一个(列表)

返回列表中的第一个元素,如果列表为空,则返回 nil。

4

展平(列表)

展平给定的嵌套列表列表。

5

insert_at(列表、索引、值)

返回一个列表,其中在指定索引处插入了值。请注意,索引的上限为列表长度。负索引表示距列表末尾的偏移量。

6

最后(列表)

返回列表中的最后一个元素,如果列表为空,则返回 nil。

元组

元组也是其中存储许多其他结构的数据结构。与列表不同,它们将元素存储在连续的内存块中。这意味着访问每个索引的元组元素或获取元组大小是一个快速操作。索引从零开始。

Elixir 使用大括号来定义元组。像列表一样,元组可以保存任何值 -

{:ok, "hello"}

元组的长度

要获取元组的长度,请使用tuple_size函数,如以下程序所示 -

IO.puts(tuple_size({:ok, "hello"}))

上述程序生成以下结果 -

2

附加值

要将值附加到元组,请使用 Tuple.append 函数 -

tuple = {:ok, "Hello"}
Tuple.append(tuple, :world)

这将创建并返回一个新元组:{:ok, "Hello", :world}

插入值

要在给定位置插入值,我们可以使用Tuple.insert_at函数或put_elem函数。考虑以下示例来理解相同的内容 -

tuple = {:bar, :baz}
new_tuple_1 = Tuple.insert_at(tuple, 0, :foo)
new_tuple_2 = put_elem(tuple, 1, :foobar)

请注意,put_eleminsert_at返回了新元组。存储在元组变量中的原始元组没有被修改,因为 Elixir 数据类型是不可变的。由于不可变,Elixir 代码更容易推理,因为您永远不需要担心特定代码是否会改变您的数据结构。

元组与列表

列表和元组有什么区别?

列表以链表的形式存储在内存中,这意味着列表中的每个元素都保存其值并指向后面的元素,直到到达列表末尾。我们将每对值和指针称为 cons 单元。这意味着访问列表的长度是一个线性操作:我们需要遍历整个列表才能计算出其大小。只要我们在前面添加元素,更新列表就会很快。

另一方面,元组连续存储在内存中。这意味着获取元组大小或通过索引访问元素的速度很快。然而,更新元组或向元组添加元素的成本很高,因为它需要复制内存中的整个元组。

Elixir - 关键字列表

到目前为止,我们还没有讨论任何关联数据结构,即可以将某个值(或多个值)与一个键关联起来的数据结构。不同的语言用不同的名称来调用这些功能,例如字典、哈希、关联数组等。

在 Elixir 中,我们有两种主要的关联数据结构:关键字列表和映射。在本章中,我们将重点关注关键字列表。

在许多函数式编程语言中,通常使用 2 项元组列表作为关联数据结构的表示。在 Elixir 中,当我们有一个元组列表并且元组的第一项(即键)是一个Atomics时,我们将其称为关键字列表。考虑以下示例来理解相同的内容 -

list = [{:a, 1}, {:b, 2}]

Elixir 支持定义此类列表的特殊语法。我们可以将冒号放在每个Atomics的末尾并完全删除元组。例如,

list_1 = [{:a, 1}, {:b, 2}]
list_2 = [a: 1, b: 2]
IO.puts(list_1 == list_2)

上述程序将生成以下结果 -

true

这两者都代表一个关键字列表。由于关键字列表也是列表,因此我们可以在它们上使用列表上使用的所有操作。

要检索与关键字列表中的Atomics关联的值,请将Atomics as 传递给列表名称后的 [] -

list = [a: 1, b: 2]
IO.puts(list[:a])

上述程序生成以下结果 -

1

关键字列表具有三个特殊特征 -

  • 键必须是Atomics。
  • 密钥是按照开发人员指定的顺序订购的。
  • 钥匙可以多次提供。

为了操作关键字列表,Elixir 提供了Keyword 模块。但请记住,关键字列表只是列表,因此它们提供与列表相同的线性性能特征。列表越长,查找键、计算项目数等所需的时间就越长。因此,Elixir 中的关键字列表主要用作选项。如果您需要存储许多项目或保证一键关联最多一个值,则应该使用映射。

访问密钥

要访问与给定键关联的值,我们使用Keyword.get函数。它返回与给定键关联的第一个值。要获取所有值,我们使用 Keyword.get_values 函数。例如 -

kl = [a: 1, a: 2, b: 3] 
IO.puts(Keyword.get(kl, :a)) 
IO.puts(Keyword.get_values(kl)) 

上述程序将生成以下结果 -

1
[1, 2]

插入钥匙

要添加新值,请使用Keyword.put_new。如果密钥已经存在,则其值保持不变 -

kl = [a: 1, a: 2, b: 3]
kl_new = Keyword.put_new(kl, :c, 5)
IO.puts(Keyword.get(kl_new, :c))

当上面的程序运行时,它会生成一个带有附加键 c 的新关键字列表,并生成以下结果 -

5

删除密钥

如果要删除某个键的所有条目,请使用Keyword.delete;要仅删除键的第一个条目,请使用Keyword.delete_first

kl = [a: 1, a: 2, b: 3, c: 0]
kl = Keyword.delete_first(kl, :b)
kl = Keyword.delete(kl, :a)

IO.puts(Keyword.get(kl, :a))
IO.puts(Keyword.get(kl, :b))
IO.puts(Keyword.get(kl, :c))

这将删除列表中的第一个b和列表中的所有a 。当上面的程序运行时,它将生成以下结果 -

0

Elixir - 地图

关键字列表是按键寻址列表中存储的内容的便捷方法,但在底层,Elixir 仍在遍历列表。如果您对该列表有其他计划需要遍历所有内容,那么这可能是合适的,但如果您计划使用键作为访问数据的唯一方法,那么这可能是不必要的开销。

这就是地图可以帮助您的地方。每当您需要键值存储时,映射就是 Elixir 中的“首选”数据结构。

创建地图

使用 %{} 语法创建地图 -

map = %{:a => 1, 2 => :b}

与关键字列表相比,我们已经可以看到两个差异 -

  • 映射允许任何值作为键。
  • 地图的键不遵循任何顺序。

访问密钥

为了访问与键关联的值,映射使用与关键字列表相同的语法 -

map = %{:a => 1, 2 => :b}
IO.puts(map[:a])
IO.puts(map[2])

当上面的程序运行时,它会生成以下结果 -

1
b

插入钥匙

要在映射中插入键,我们使用Dict.put_new函数,它将映射、新键和新值作为参数 -

map = %{:a => 1, 2 => :b}
new_map = Dict.put_new(map, :new_val, "value") 
IO.puts(new_map[:new_val])

这将在新映射中插入键值对:new_val - "value" 。当上面的程序运行时,它会生成以下结果 -

"value"

更新值

要更新地图中已有的值,您可以使用以下语法 -

map = %{:a => 1, 2 => :b}
new_map = %{ map | a: 25}
IO.puts(new_map[:a])

当上面的程序运行时,它会生成以下结果 -

25

模式匹配

与关键字列表相比,地图对于模式匹配非常有用。当在模式中使用映射时,它将始终匹配给定值的子集 -

%{:a => a} = %{:a => 1, 2 => :b}
IO.puts(a)

上述程序生成以下结果 -

1

这会将a1匹配。因此,它将生成输出1

如上所示,只要模式中的键存在于给定映射中,映射就会匹配。因此,空地图可以匹配所有地图。

访问、匹配和添加映射键时可以使用变量 -

n = 1
map = %{n => :one}
%{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}

Map 模块提供了与 Keyword 模块非常相似的 API,具有操作地图的便捷功能。您可以使用Map.get、Map.delete等函数来操作地图。

带有 Atom 键的地图

地图具有一些有趣的属性。当映射中的所有键都是Atomics时,为了方便起见,您可以使用关键字语法 -

map = %{:a => 1, 2 => :b} 
IO.puts(map.a) 

映射的另一个有趣的属性是它们提供自己的语法来更新和访问Atomics键 -

map = %{:a => 1, 2 => :b}
IO.puts(map.a)

上述程序生成以下结果 -

1

请注意,要以这种方式访问​​Atomics键,它应该存在,否则程序将无法工作。

Elixir - 模块

在 Elixir 中,我们将多个功能分组为模块。我们在前面的章节中已经使用过不同的模块,例如 String 模块、Bitwise 模块、Tuple 模块等。

为了在 Elixir 中创建我们自己的模块,我们使用defmodule宏。我们使用def宏来定义该模块中的函数 -

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

在接下来的部分中,我们的示例将变得更长,并且在 shell 中将它们全部键入可能会很棘手。我们需要学习如何编译 Elixir 代码以及如何运行 Elixir 脚本。

汇编

将模块写入文件总是很方便,以便可以编译和重用它们。假设我们有一个名为 math.ex 的文件,其中包含以下内容 -

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

我们可以使用命令 - Elixirc编译文件:

$ elixirc math.ex

这将生成一个名为Elixir.Math.beam的文件,其中包含已定义模块的字节码。如果我们再次启动iex,我们的模块定义将可用(前提是 iex 在字节码文件所在的同一目录中启动)。例如,

IO.puts(Math.sum(1, 2))

上述程序将生成以下结果 -

3

脚本模式

除了 Elixir 文件扩展名.ex之外,Elixir 还支持.exs文件用于脚本编写。Elixir 以完全相同的方式处理这两个文件,唯一的区别在于目标。.ex文件用于编译,而 .exs 文件用于编写脚本。执行时,两个扩展都会编译并将其模块加载到内存中,尽管只有.ex文件以 .beam 文件的格式将其字节码写入磁盘。

例如,如果我们想在同一个文件中运行 Math.sum 我们可以按以下方式使用 .exs -

数学表达式

defmodule Math do
   def sum(a, b) do
      a + b
   end
end
IO.puts(Math.sum(1, 2))

我们可以使用 Elixir 命令运行它 -

$ elixir math.exs

上述程序将生成以下结果 -

3

该文件将在内存中编译并执行,并打印“3”作为结果。不会创建字节码文件。

模块嵌套

Elixir 中可以嵌套模块。该语言的这一特性可以帮助我们以更好的方式组织代码。要创建嵌套模块,我们使用以下语法 -

defmodule Foo do
   #Foo module code here
   defmodule Bar do
      #Bar module code here
   end
end

上面给出的示例将定义两个模块:FooFoo.Bar。第二个可以作为Foo内的Bar进行访问,只要它们位于相同的词法范围内。如果稍后将Bar模块移到 Foo 模块定义之外,则必须通过其全名 (Foo.Bar) 引用它,或者必须使用别名章节中讨论的别名指令设置别名。

注意- 在 Elixir 中,不需要定义 Foo 模块来定义 Foo.Bar 模块,因为该语言将所有模块名称翻译为Atomics。您可以定义任意嵌套的模块,而无需在链中定义任何模块。例如,您可以定义Foo.Bar.Baz而不定义FooFoo.Bar

Elixir - 别名

为了方便软件重用,Elixir 提供了三个指令——alias 、requireimport。它还提供了一个名为 use 的宏,总结如下 -

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar

# Ensure the module is compiled and available (usually for macros)
require Foo

# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo

# Invokes the custom code defined in Foo as an extension point
use Foo

现在让我们详细了解每个指令。

别名

别名指令允许