第1章 接受现实
UNIX用户推崇的设计理念,是将命令行工具设计得小巧、功能单一。这些工具可以任意地组合起来使用。每个工具都是获取输入,转换输入内容,并以下一个工具(或人类)能够使用的格式输出结果。
这种设计理念非常灵活而且带来了极好的重用性。UNIX工具能够以其设计者预想不到的方式进行组合。而且每个工具都给其他工具添加了更多的可能性。
这种设计理念还是高可靠的–每个小程序只做好一件事,这就使测试变得更加容易。
还一个好处,就是命令管道可以并行工作。
$ grep Elixir *.pml | wc -1
grep命令运行的时候,字数统计程序wc也在运行。因为wc同步地处理grep的输出,所以一旦grep执行完毕,计算结果也就出来了。
Elixir 让我们以类似于UNIX shell的方式来解决问题,但不是用命令行工具,而是用函数。而且如果我们愿意,可以将它们串联起来。那些函数越小巧,功能越单一,组合以后的灵活性就越大。
我们甚至可以让这些函数以并行的方式运行-Elixir拥有简单而强大的机制来让它们能够彼此传递消息。而这些机制并非老掉牙的进程或线程–我们讨论的是允许单台机器上运行几百万个实例而且有几百台这样的机器进行交互的能力。BruceTate对此的想法是:“许多程序员将线程和进程看作是罪恶之源;Elixir开发者则认为它们是重要的简化机制。”
数据转换的理念是函数式编程的核心:函数将输入转换成输出。三角函数sin就是一个例子-给它输入$ pi/4$,你就会得到0.7071……。HTML模板系统也是函数;它接受包含占位符的模板和一组命名值,并生成完整的HTML文档。
换一种方式思考:
- 面向对象不是设计代码的唯一方法
- 函数式编程不一定是复杂的和纯数学的
- 编程的基础不是赋值、if语句和循环
- 并发不一定需要锁、信号量、监视器等类似的东西
- 进程不必消耗大量的资源
- 元编程不只是语言的附属品
- 即使编程是你的工作,也应该是充满乐趣的
第2章 模式匹配
Elixir 核心哲学中最重要、最颠覆传统语言的概念:模式匹配(Pattern Matching)。
在 Elixir 中,= 不是赋值符号,而是“匹配操作符”(Match Operator)。它更像数学里的等号(方程两边平衡),它的工作是:让左边的模式(Pattern)和右边的值(Value)进行匹配。
我们来逐行复盘你的实验,看看底层发生了什么:
a=1 $\rightarrow$ 成功(绑定)
- 动作: 左边是一个没有被绑定的变量 a,右边是数字 1。
- 底层逻辑: 当左边是一个干净的变量时,模式匹配必定成功。作为成功的副作用,值 1 被绑定到了变量 a 上。所以 IEx 返回了结果 1。
1=a $\rightarrow$ 成功(断言)
- 动作: 左边是数字字面量 1,右边是变量 a。
- 底层逻辑: 此时 a 里面已经是 1 了,所以等式变成了 1 = 1。左边的字面量 1 成功匹配了右边的 1。这里并没有发生赋值,而是进行了一次断言(Assertion)。因为两边相等,所以顺利通过。
1=2 $\rightarrow$ 报错 (MatchError)
- 动作: 左边是 1,右边是 2。
- 底层逻辑: 匹配器试图让 1 等于 2。因为字面量 1 无法匹配字面量 2,两边无法平衡,所以无情抛出 MatchError(模式匹配错误),告诉你右边的值 2 没办法塞进左边的模式 1 里。
2=a $\rightarrow$ 报错 (MatchError)
- 动作: 左边是 2,右边是当前值为 1 的变量 a。
- 底层逻辑: 此时等式相当于 2 = 1。由于左边是固定的字面量 2,它要求右边必须也是 2 才能匹配成功。但右边是 1,所以再次引发 MatchError。这充分证明了 = 左边如果是具体的值,它是没有赋值能力的。
a=2 $\rightarrow$ 成功(重绑定 / Rebinding)
- 动作: 左边是变量 a,右边是数字 2。
- 底层逻辑: 很多函数式编程语言(如 Erlang)是不允许变量改变的(只能绑定一次)。但 Elixir 允许变量重绑定。当左边又是孤零零的变量 a 时,匹配再次无条件成功,a 抛弃了旧值 1,重新绑定了新值 2。
在实际开发中,我们很少拿它来单纯比对数字,而是用来解构复杂的数据结构(比如元组 Tuple 或列表 List):
比如一个获取用户信息的函数返回了一个元组:{:ok, "张三"},我们可以这样匹配并直接拿到名字:
{:ok, name} = {:ok, "张三"},此时变量 name 就会被自动绑定为 "张三", IO.puts(name) 输出 "张三"
在Elixir里,等号不是赋值,而更像一种断言(assertion)。如果Elixir可以找到一种方式让等号的左边等于右边,则执行成功。Elixir将等号“=”称为匹配运算符(match operator)
iex> list=[1,2,[3,4,5]]
[1, 2, [3, 4, 5]]
iex> [a,b,c]=list
[1, 2, [3, 4, 5]]
iex> a
1
iex> b
2
iex> c
[3, 4, 5]
Elixir会试图让等号的左边等于右边。左边列表包含3个变量,而右边列表有3个值,所以将值设置到相应变量,等号左右两边才相等。Elixir 将这个处理过程称为模式匹配(pattermatching)。模式匹配只有在其值与模式的结构相同,而且模式中的项目与值中对应的项目都匹配时才算成功。
用(下画线 _)忽略匹配值:如果我们不需要获取匹配值,可以使用特殊变量_(下画线)。它类似于一般变量,只不过它会立即丢弃提供给它的任何值。在匹配过程中,它就像一个通配符,声称“我可以接受任何值”。下面的例子匹配所有包含3个元素并且第1个元素的值为1的列表:
iex> [a,_,_]=[1,2,3]
[1, 2, 3]
iex> a
1
每次匹配变量仅绑定一次:在匹配过程中,变量一旦被绑定为某个值,那么该值在匹配其余部分的时候就会保持不变。
iex> [a,a]=[1,1]
[1, 1]
iex> [a,a]=[1,2]
** (MatchError) no match of right hand side value:
[1, 2]
(stdlib 8.0.3) erl_eval.erl:796: :erl_eval.expr/6
iex:15: (file)
如果让变量已有值强制参与匹配(不修改变量的值),使用^前缀
iex> a=2
2
iex> ^a=1
** (MatchError) no match of right hand side value:
1
(stdlib 8.0.3) erl_eval.erl:796: :erl_eval.expr/6
iex:16: (file)
iex> [^a,2,3]=[1,2,3]
** (MatchError) no match of right hand side value:
[1, 2, 3]
(stdlib 8.0.3) erl_eval.erl:796: :erl_eval.expr/6
iex:16: (file)
iex> [a,2,3]=[1,2,3]
[1, 2, 3]
iex> a
1
iex>
第3章 不可变性
不可变的数据才是已知的:在Elixir中,所有值都是不可变的。最复杂的嵌套列表、数据库记录–这些东西的行为就跟最简单的整型值一样。它们的值都是不可变的。在Elixir里,变量一旦引用了一个列表,如[1,2,3],你就知道它总是引用相同的值(直到你重新绑定变量为止)。而这就使得并发变得没那么恐怖了。
假若你需要给[1,2,3]里的每个元素加上100呢?Elixir会基于原始数据生成包含新数据的副本,原始数据则保持不变,而你的操作也不会影响到其他持有原始数据引用的代码。
实验一:复杂的嵌套列表,就跟数字 1 一样稳固
在传统语言(如 Python 或 JavaScript)中,如果你把一个列表赋值给另一个变量,修改其中一个,另一个也会跟着变(因为它们共享同一个内存指针,这在并发时是灾难)。但在 Elixir 中,所有值都是绝对不可变的:
# 1. 创建一个复杂的嵌套列表,绑定到变量 list_a
iex> list_a = [1, [2, 3], "nested"]
[1, [2, 3], "nested"]
# 2. 把 list_a “赋值”(引用)给 list_b
iex> list_b = list_a
[1, [2, 3], "nested"]
# 3. 此时我们尝试调用 List 模块的函数,往 list_a 里面添加一个新元素
iex> List.insert_at(list_a, 0, "new_value")
["new_value", 1, [2, 3], "nested"]
# 4. 【见证奇迹的时刻】重新查看 list_a 和 list_b 的值:
iex> list_a
[1, [2, 3], "nested"]
iex> list_b
[1, [2, 3], "nested"]
没有任何函数能够真正“修改” list_a 的内部数据。List.insert_at 并没有在原地修改列表,而是在内存里高效复制并生成了一个全新的列表返回给你。list_a 和 list_b 引用的内容,从它们诞生的那一刻起,就永远被钉死在内存里了,绝对不会改变。这行为就和数字 1 + 1 返回 2,但数字 1 本身没有变成 2 是一模一样的。
实验二:变量重绑定,改变的是“标签”,而不是“数据”
# 1. 我们先让 list_b 记住当前的 list_a
iex> list_b = list_a
[1, [2, 3], "nested"]
# 2. 对 list_a 进行“重绑定”
iex> list_a = [9, 9, 9]
[9, 9, 9]
# 3. 看看刚才共享同一份数据的 list_b 变了没有?
iex> list_b
[1, [2, 3], "nested"]
在 Elixir 中,变量仅仅是一个“指向内存中某块数据的标签(指针)”。当你执行 list_a = [9, 9, 9] 时,你只是撕下了 list_a 这个标签,把它贴到了新数据 [9, 9, 9] 上。而原本那块 [1, [2, 3], "nested"] 的内存数据依然完好无损地躺在那里,被 list_b 牢牢引用着。数据本身,从未被修改过。
用不可变数据编写程序:一旦接受了这个概念,用不可变数据写程序就非常容易了。你只要记住,任何转换数据的函数都会返回新的数据副本。这样,我们永远都不是将字符串的首字母转换成大写,而是返回一份首字母转换成大写的字符串副本。
iex> name="elixir"
"elixir"
iex> cap_name=String.capitalize(name)
"Elixir"
iex> cap_name
"Elixir"
iex> name
"elixir"
如果你以前用面向对象语言编程,可能不喜欢我们将代码写成String.capitalizename而不是name.capitalize()。但在面向对象的语言里,对象大多拥有可变状态。当做 name.capitalize()这样的调用的时候,你无法得知是否已经修改了 name的内部状态,返回的是否是字符串首字母大写的副本,或两者都无法得知。情况变得非常模棱两可。
在函数式语言里,我们总是转换数据。我们从不就地修改它。每次使用这个语法的时候,都要记住这一点。
第4章 Elixir基础
Elixir的内置类型有:
- 值类型
- 任意大小的整数
- 浮点数:双精度 64 位浮点数,遵守 IEEE 754 标准,用于处理小数
- 原子:Elixir 灵魂级的值类型。其名字就是它的值(如 :ok, :error)。它们在内存中会被注册进一个“原子表”,相同名字的原子在内存里只有一份,因此比对速度极快(本质是比对内存整数地址),是模式匹配的绝对主力。
- 区间(技术上讲不是值类型,底层实现是结构体)
- 正则表达式(技术上讲不是值类型,底层实现是结构体)
- 系统类型
- PID和端口
- 引用
- 集合类型
- 元组
- 列表
- 散列表
- 二进制型
原子是常量,用于表示某些东西的名字。它以冒号(:)开头,其后跟随一个原子单词或者Elixir运算符。原子单词由字母、数字、下画线和符号“@”组成,以感叹号或者问号结尾。可以创建一个由任意字符序列组成的原子,只要其包含在双引号里并以冒号开头。下面都是原子类型的数据:
:fred
:is binary?
:var@2
:<>
:===
:"func/3"
:"long john silver"
原子的名字就是它的值。两个同名原子总是相等的,即使它们是由相隔万里的两台不同计算机的不同应用程序创建的。我们会大量使用原子来标记值。
区间被表示为开始..结束,开始和结束可以是任意类型值。但是,如果你想遍历区间中的值,其两端必须是整数。
# range_demo.exs
IO.puts("=== 1. 整数区间演示 ===")
int_range = 1..5
IO.puts("成功创建整数区间: #{inspect(int_range)}")
# 整数区间可以完美进行遍历
doubled_list = Enum.map(int_range, fn x -> x * 2 end)
IO.puts("遍历翻倍后的结果: #{inspect(doubled_list)}")
# 输出
#=== 1. 整数区间演示 ===
#成功创建整数区间: 1..5
#遍历翻倍后的结果: [2, 4, 6, 8, 10]
系统类型:这些类型对应着Erlang虚拟机的底层资源。
- PID和端口: PID是本地或者远端进程的引用,而端口是读/写资源(通常是对应用程序外部)的引用。当前进程的PID值可以通过调用self获得。当一个新进程被创建时会得到一个新的PID。
- 引用:make_ref 函数用于创建全局唯一的引用;任何其他引用都不会与之相等。本书不涉及如何使用引用。
元组
元组表示一组有序元素的集合。如同所有的Elixir数据结构,一旦元组被创建就无法修改。元组写在一对花括号内,其元素用逗号隔开。
{ 1, 2 } { :ok, 42,"next" } { :error, :enoent }
通常函数调用在不出错的情况下会返回一个元组,其第一个元素为原子:ok。例如
{:ok,#PID<0.39.0>}
列表
列表的字面量语法: [1,2,3]。这也许会让你认为列表和其他语言的数组类似,但是它不是(事实上,Elixir 里的元组类型更接近于通常意义的数组)。相反,列表是一个链式数据结构。列表要么为空,要么由首部和尾部组成。首部包含一个值,尾部本身是一个列表。
如第7章“列表和递归”所述,递归定义的列表是Elixi编程的核心部分之一。
考虑其实现特征,列表适合线性遍历,随机访问列表会很费时(为了获得第n个元素,不得不扫描前n-1个元素)。但取得列表的首部并提取出尾部总是很方便。
列表还有另外一个性能优势。一旦列表被构造出来,就永远不会发生变化。所以,假如要剔除列表的首部,仅留下尾部,就不再需要拷贝列表,仅返回尾部的指针即可。这是所有列表遍历技巧的基础,我们将在第7章详细说明。
Elixir有些运算符只对列表有效:
iex> [1,2,3]++[4,5,6] #连接
[1, 2, 3, 4, 5, 6]
iex> [1,2,3,4]--[2,4] #差集
[1, 3]
iex> 1 in [1,2,3,4] #属于
true
iex> 5 in [1,2,3,4]
false
关键字列表
因为我们常常需要一组键值对,Elixir提供了一个简写方式,如果这样写:
[ name: "Dave", city: "Dallas", likes: "Programming" ]
Elixir将其转换为一个由双值元组组成的列表:
[ {:name, "Dave"}, {:city, "Dallas"}, {:likes, "Programming"} ]
- 这里
{:name, "Dave"}语法糖写法name: "Dave" - 当关键字列表作为函数调用的最后一个参数时,Elixir允许去掉方括号。
DB.save record, [ {:use_transaction, true}, {:logging, "HIGH"} ]
可以更简洁地写成:
DB.save record, use_transaction: true, logging: "HIGH"
散列表
散列表是一个键值对的集合。散列表的字面量形如:
%{ key => value, key => value }
下面是一些散列表:
iex> cites=%{"BJ"=>"北京","SH"=>"上海"}
%{"BJ" => "北京", "SH" => "上海"}
iex> responses = %{{ :error, :enoent } => :fatal, { :error, :busy } => :retry }
%{{:error, :busy} => :retry, {:error, :enoent} => :fatal}
iex> colors = %{ :red => 0xff0000, :green => 0x00ff00, :blue => 0x0000ff }
%{green: 65280, blue: 255, red: 16711680}
第一个例子,键是字符串;第二个例子,键是元组;第三个例子,键是原子。
通常情况下,同一散列表中键类型相同,但也不尽然。
iex> %{ "one" => 1, :two => 2, {1,1,1} => 3 }
%{:two => 2, {1, 1, 1} => 3, "one" => 1}
如果键是原子类型,可以使用类似于关键字列表的简略写法:
iex> colors = %{ red: 0xff0000, green: 0x00ff00, blue: 0x0000ff }
%{green: 65280, blue: 255, red: 16711680}
为何既有散列表又有关键字列表呢? 散列表的每个条目的键是不同的,而关键字列表允许存在重复的键。散列表更高效(尤其其条目数量不断增加时),并且可用于Elixir的模式匹配(我们会在第5章讨论)。通常,我们用关键字列表来处理命令行参数和传递选项,而想获得关联数组的时候就用散列表(或另一个数据结构,HashDict)。
散列表可以通过键获取值。方括号语法对所有散列表都有效:
iex> cites=%{"BJ"=>"北京","SH"=>"上海"}
%{"BJ" => "北京", "SH" => "上海"}
iex> cites["SH"]
"上海"
iex> cites["XX"]
nil
iex> responses = %{{ :error, :enoent } => :fatal, { :error, :busy } => :retry }
%{{:error, :busy} => :retry, {:error, :enoent} => :fatal}
iex> responses[{ :error, :enoent }]
:fatal
如果键是原子类型,还可以使用点符号.使用点符号的时候,如果没有相匹配的键,将得到一个KeyError。
iex> colors = %{ red: 0xff0000, green: 0x00ff00, blue: 0x0000ff }
%{green: 65280, blue: 255, red: 16711680}
iex> colors.blue
255
iex> colors.bluee
** (KeyError) key :bluee not found in:
%{green: 65280, blue: 255, red: 16711680}
Did you mean:
* :blue
iex:16: (file)
二进制型
有时你需要访问的数据是一个位或者字节的序列,比如,JPEG和MP3文件的头部包含一些字段,可能在单个字节内放置两到三个值。Elixir通过二进制型来支持此类操作。二进制型字面量被放置在<<和>>之间,此基本表示法将连续的整数逐个转换成字节:
iex> bin =<<1,2>>
<<1, 2>>
iex> byte_size bin
2
在计算机中,所有数据最终都是由 0 和 1 组成的。
- 1 个位 (bit) = 最小的存储单位(只能是 0 或 1)。
- 1 个字节 (byte) = 8 个位(可以表示 0 到 255 之间的整数)。
例子中的 bin = <<1, 2>>,在计算机内存里的本质是一串连续的二进制数字:
- 1 占用 1 个字节:00000001
- 2 占用 1 个字节:00000010
所以内存里实际躺着的是:0000000100000010。因为总共占用了 2 个字节,所以 byte_size(bin) 的结果是 2。
普通的编程语言通常以“字节”或“对象” animate 为单位处理数据。但像 JPEG 图片或 MP3 音频的头部(Header),为了极大地节省空间,设计者会在一个字节内部塞入好几个不同的控制信息。
例如,一个 8 位的字节中:
- 前 3 位表示“音频质量”
- 中间 2 位表示“声道数”
- 后 3 位表示“采样率”
我们可以使用修饰符来精确控制每个字段的类型和位宽(大小)。
例如,下面我们将 3 个字段打包进单个字节(8 位)中,它们的长度分别为 2 位、4 位和 2 位。通过内置库函数,我们可以直观地看到它在内存中的二进制形态:
# 3 的二进制是 11 (2位),5 是 0101 (4位),1 是 01 (2位)
# 拼接后组合成 11010101,对应的十进制数正是 213
iex> bin = <<3::size(2), 5::size(4), 1::size(2)>>
<<213>>
# 将该字节转换成二进制字符串打印出来
iex> :io.format("~-8.2b~n", :binary.bin_to_list(bin))
11010101
:ok
# 查看占用的字节数
iex> byte_size(bin)
1
二进制型在 Elixir 中极其重要却又深藏不露。说它重要,是因为 Elixir 的底层字符串(UTF-8)完全基于它构建;说它深藏不露,是因为在日常开发中,Elixir 强大的高层抽象让你几乎不需要直接参与底层的位运算。
Elixir 基础编码规范:命名、源码与风格约定
1. 标识符与命名规则
- 基础构成:标识符由大小写字母、数字和下划线组成,允许以问号 ?(通常表示返回布尔值)或感叹号 !(通常表示会抛出异常或进行破坏性操作)结尾。 [1]
- 大驼峰命名 (UpperCamelCase):适用于模块 (Modules)、记录 (Records)、协议 (Protocols) 和 行为 (Behaviors)。 [2]
- 蛇形命名 (snake_case):其他所有标识符(如变量名、函数名等)以小写字母或下划线开头,单词间用下划线分隔。 [2]
- 匿名变量约定:若变量以下划线开头(如 _data),即使它在模式匹配或函数参数列表中未被使用,Elixir 编译器也不会触发“未使用变量”的警告。
2. 源码文件规范
- 字符编码:源文件必须使用 UTF-8 编码,但标识符本身仅允许使用 ASCII 字符。
- 缩进与排版:社区习惯使用 两格空格缩进,禁止使用制表符 (Tab)。(注:现代开发中直接使用内置的 mix format 工具即可自动规范)。
- 代码注释:使用井号 # 作为单行注释的起点,直至行尾。 [3]
3. 社区风格指南
目前 Elixir 社区广泛认可并遵循的风格规范主要有:
- 由 christopheradams 发起的社区驱动版 Elixir 风格指南。
- 贴近 Elixir 核心团队源码风格的 lexmag 版 Elixir 风格指南。
- 由静态代码分析工具 Credo 实现的 rrrene 官方风格指南。 [4, 5, 6, 7]
- [1] https://hexdocs.pm
- [2] https://elixirforum.com
- [3] https://github.com
- [4] https://github.com
- [5] https://elixirforum.com
- [6] https://github.com
- [7] https://github.com
命名规范(Identifiers)
- 基本组成:由大小写字母、数字和下划线组成。
- 特殊结尾:可以以问号(?)或感叹号(!)结尾。
- 大驼峰命名法(BumpyCase):适用于模块、记录、协议和行为的名称,必须以大写字母开头。
- 小写/下划线开头:其他标识符以此开头,通常用下划线分隔单词。
- 划线特殊规则:如果首字母是下划线,且该变量在模式匹配和函数参数列表中未被使用,Elixir 不会发出警告。
源文件与编码(Source Files & Encoding)
- 文件编码:源文件使用 UTF-8 编码。
- 标识符限制:标识符只能使用 ASCII 字符。
排版与注释约定(Conventions)
- 缩进:习惯上采用 两格缩进。
- 空白符:使用 空格 而非制表符(Tab)。
- 注释:以 井号(#) 开始,直到行尾。
Elixir 语言中的真值(Truthiness)与布尔值
- 三个关键值:Elixir 中与布尔计算相关的三个核心值是 true、false 和 nil。
- 空值(nil)的处理:在布尔上下文中,nil 会被当作 false 来对待。
- 原子类型本质:这三个值本质上都是与它们同名的原子类型(Atom)的值。例如:true 和原子类型 :true 是完全等价的(同理,false 等价于 :false,nil 等价于 :nil)。
- 真值(Truthy)规则:在绝大多数上下文中,除了 false 和 nil 以外的所有值都被当作“真”。为了与严格的 true 做出区分,这种特性通常被称为 "truthy"(真值)。
运算符
比较运算符
Elixir 支持以下核心比较运算符,其中对于整数和浮点数的处理有严格与宽泛之分:
| 运算符 | 描述 | 示例与结果 |
|---|---|---|
| a === b | 严格相等(同时比较值和类型) | 1 === 1.0 结果为 假 (false) |
| a !== b | 严格不等(值或类型不同即为真) | 1 !== 1.0 结果为 真 (true) |
| a == b | 值相等(允许跨数值类型比较值) | 1 == 1.0 结果为 真 (true) |
| a != b | 值不等 | 1 != 1.0 结果为 假 (false) |
| a > b | 大于(正常大小比较) | — |
| a >= b | 大于等于 | — |
| a < b | 小于 | — |
| a <= b | 小于等于 | — |
跨类型结构化排序 (Structural Comparison)
在 Elixir 中,你可以比较任意两个不同类型的值。如果两个值的类型不同或不兼容,Elixir 会基于以下固定的类型优先级次序(从小到大)决定大小:
$\text{number} < \text{atom} < \text{reference} < \text{function} < \text{port} < \text{pid} < \text{tuple} < \text{map} < \text{list} < \text{binary}$
- 应用场景:这使得我们在对混合数据的集合进行排序算法(如 Enum.sort/1)时,永远不会因为类型不匹配而报错。
- 示例:根据上述规则,任何数字(number)都小于任何原子(atom),因此 99 < :apple 的结果为 真 (true)。
布尔运算符 (Strict Boolean Operators)
这类运算符严格要求第一个参数必须是布尔值(true 或 false),否则会抛出类型错误(ArgumentError)。它们具备短路求值(Short-circuiting)特性:
- a or b:逻辑或。若 a 包含真值则直接返回 true(不计算 b);否则继续求值并返回 b。
- a and b:逻辑与。若 a 包含假值则直接返回 false(不计算 b);否则继续求值并返回 b。
- not a:逻辑非。若 a 为真则返回 false,否则返回 true。
松弛布尔运算符 (Relaxed Boolean Operators)
这类运算符接受任意类型的值作为参数。它遵循前面提到的 Truthy(真值) 规则:任何除了 nil 或 false 以外的值都被解释为真。
- a || b:若 a 为真值(Truthy)则返回 a;否则返回 b。
- a && b:若 a 为真值(Truthy)则返回 b;否则返回 a (注:原书中此处注释“否则为 b”有误,实际运行中若 a 为假值则会返回 a 本身,如 nil && :apple 返回 nil)。
- !a:若 a 为真值(Truthy)则返回 false;否则返回 true。
算术运算符 (Arithmetic Operators)
Elixir 支持基础的算术运算 +、-、*、/,并内置了专门的整数操作函数:
- /(常规除法):无论参数是否为整数,结果永远返回浮点数(例如 10 / 2 结果为 5.0)。
- div(a, b)(整数除法):用于截断小数部分,只求得整数结果(例如 div(10, 3) 结果为 3)。
- rem(a, b)(求余运算):以函数形式调用,返回除法的余数(例如 rem(11, 3) => 2)。其结果的符号与第一个参数(被除数)相同。
连接与集合运算符 (Concatenation & Collection Operators)
Elixir 针对不同的数据类型提供了专用的连接和操作符:
- binary1 <> binary2:连接两个二进制型(在 Elixir 中,字符串本质上就是二进制型,因此这也是字符串拼接的操作符)。
- list1 ++ list2:连接两个列表,将右侧列表拼接到左侧列表末尾。
- list1 — list2:列表差集。返回存在于 list1 中但已被 list2 移除(按顺序和次数抵消)后的元素。
in 运算符 (Membership Operator)
- a in enum:成员检查。测试元素 a 是否被包含在某个枚举类型(如列表 [1, 2, 3] 或区间 1..10)之中,返回布尔值。