查看源代码 语法参考

Elixir 语法旨在与抽象语法树 (AST) 有直接的转换。这意味着 Elixir 语法在很大程度上是统一的,并带有少量“语法糖”结构以减少常见 Elixir 习语中的噪音。

本文档涵盖了所有 Elixir 语法结构作为参考,然后讨论其精确的 AST 表示。

保留字

这些是 Elixir 语言中的保留字。它们在本指南中都有详细介绍,这里总结起来是为了方便使用。

  • true, false, nil - 用作原子
  • when, and, or, not, in - 用作运算符
  • fn - 用于匿名函数定义
  • do, end, catch, rescue, after, else - 用于 do-end 块

数据类型

数字

Elixir 中的整数 (1234) 和浮点数 (123.4) 表示为数字序列,可以为了可读性而使用下划线分隔,例如 1_000_000。整数在其表示中从不包含小数点 (.)。浮点数包含小数点和小数点后至少一位数字。浮点数还支持科学计数法,例如 123.4e10123.4E10

原子

未引用的原子以冒号 (:) 开头,冒号后必须紧跟 Unicode 字母或下划线。原子可以继续使用 Unicode 字母、数字、下划线和 @ 的序列。原子可以以 !? 结尾。有效的未引用的原子是::ok, :ISO8601, 和 :integer?

如果冒号后紧跟一对双引号或单引号包围的原子名称,则原子被认为是引用的。与未引用的原子相比,此原子可以包含任何 Unicode 字符(不仅仅是字母),例如 :'🌢 Elixir', :"++olá++", 和 :"123"

具有相同名称的引用和未引用原子被视为等效的,因此 :atom, :"atom", 和 :'atom' 表示相同的原子。唯一的区别是,当在不需要引用的原子中使用引号时,编译器会发出警告。

Elixir 中的所有运算符也是有效的原子。有效的示例是 :foo, :FOO, :foo_42, :foo@bar, 和 :++。无效的示例是 :@foo (@ 不允许在开头), :123 (数字不允许在开头), 和 :(*) (不是有效的运算符)。

true, false, 和 nil 是分别由原子 :true, :false:nil 表示的保留字。

要了解有关原子中允许的所有 Unicode 字符的更多信息,请参阅 Unicode 语法 文档。

字符串

Elixir 中的单行字符串用双引号括起来,例如 "foo"。字符串内的任何双引号必须使用 \ 转义。字符串支持 Unicode 字符,并存储为 UTF-8 编码的二进制文件。

Elixir 中的多行字符串用三个双引号括起来,并且可以在其中包含未转义的引号。生成的字符串将以换行符结尾。最后一个 """ 的缩进用于从内部字符串中删除缩进。例如

iex> test = """
...>     this
...>     is
...>     a
...>     test
...> """
"    this\n    is\n    a\n    test\n"
iex> test = """
...>     This
...>     Is
...>     A
...>     Test
...>     """
"This\nIs\nA\nTest\n"

字符串始终在 AST 中表示为自身。

字符列表

Elixir 中的字符列表用单引号括起来,例如 'foo'。字符串内的任何单引号必须使用 \ 转义。字符列表由非负整数构成,每个整数代表一个 Unicode 代码点。

多行字符列表用三个单引号 (''') 括起来,与多行字符串的方式相同。

字符列表始终在 AST 中表示为自身。

有关更深入的信息,请阅读 List 模块中的“字符列表”部分。

列表、元组和二进制文件

列表、元组和二进制文件等数据结构分别由定界符 [...], {...}, 和 <<...>> 标记。每个元素用逗号分隔。允许使用尾随逗号,例如在 [1, 2, 3,] 中。

映射和关键字列表

映射使用 %{...} 符号,每个键值对由用 => 标记的键值对给出,例如 %{"hello" => 1, 2 => "world"}

具有原子键的关键字列表(包含两个元素元组的列表,其中第一个元素是原子)和映射都支持关键字符号,其中冒号字符 : 被移动到原子的末尾。 %{hello: "world"} 等效于 %{:hello => "world"},而 [foo: :bar] 等效于 [{:foo, :bar}]。此符号是一种语法糖,它发出相同的 AST 表示。它将在后面的部分中解释。

结构体

结构体是在映射语法上构建的,通过在 %{ 之间传递结构体名称来实现。例如,%User{...}

表达式

变量

Elixir 中的变量必须以下划线或非大写或首字母大写的 Unicode 字母开头。变量可以继续使用 Unicode 字母、数字和下划线的序列。变量可以以 ?! 结尾。要了解有关变量中允许的所有 Unicode 字符的更多信息,请参阅 Unicode 语法 文档。

Elixir 的命名约定 建议将变量使用 snake_case 格式。

非限定调用(本地调用)

非限定调用,例如 add(1, 2),必须以字符开头,然后遵循与变量相同的规则,这些规则后面可选地跟着括号,然后是参数。

零元调用(即没有参数的调用)需要括号,以避免与变量的歧义。如果使用括号,它们必须紧跟在函数名称后面 *没有空格*。例如,add (1, 2) 是语法错误,因为 (1, 2) 被视为无效的块,试图将其作为单个参数传递给 add

Elixir 的命名约定 建议将调用使用 snake_case 格式。

运算符

与许多编程语言一样,Elixir 也支持运算符作为非限定调用,并具有其优先级和结合性规则。 =, when, &@ 等结构只是被视为运算符。有关完整参考,请参阅 运算符页面

限定调用(远程调用)

限定调用,例如 Math.add(1, 2),必须以字符开头,然后遵循与变量相同的规则,这些规则后面可选地跟着括号,然后是参数。限定调用还支持运算符,例如 Kernel.+(1, 2)。Elixir 还允许函数名称用双引号或单引号括起来,允许在引号之间使用任何字符,例如 Math."++add++"(1, 2)

与非限定调用类似,括号对零元调用(即没有参数的调用)具有不同的含义。如果使用括号,例如 mod.fun(),则表示函数调用。如果省略括号,例如 map.field,则表示访问映射的字段。

Elixir 的命名约定 建议将调用使用 snake_case 格式。

别名

别名是在编译时扩展为原子的结构。别名 String 扩展为原子 :"Elixir.String"。别名必须以 ASCII 大写字母开头,后面可以跟任何 ASCII 字母、数字或下划线。别名中不支持非 ASCII 字符。

多个别名可以用 . 连接起来,例如 MyApp.String,它扩展为原子 :"Elixir.MyApp.String"。点实际上是名称的一部分,但也可以用于组合。如果您在代码中定义了 alias MyApp.Example, as: Example,那么 Example 将始终扩展为 :"Elixir.MyApp.Example",而 Example.String 将扩展为 :"Elixir.MyApp.Example.String"

Elixir 的命名约定 建议将别名使用 CamelCase 格式。

模块属性

模块属性是特定于模块的存储,并以一元运算符 @ 与变量和本地调用的组合形式编写。例如,要写入名为 foo 的模块属性,请使用 @foo "value",并使用 @foo 从中读取。鉴于模块属性是使用现有结构编写的,因此它们遵循上述针对运算符、变量和本地调用的相同规则。

块是用换行符或分号分隔的多个 Elixir 表达式。可以使用括号随时创建新的块。

左箭头

左右箭头 (->) 用于在左右两侧建立关系,通常称为子句。左侧可以有零个、一个或多个参数;右侧是零个、一个或多个由换行符分隔的表达式。-> 可以在以下终止符之间出现一次或多次:do-endfn-end(-)。当使用 -> 时,在这些终止符之间只允许其他子句。混合子句和正则表达式是非法的语法。

它出现在 casecond 结构中,位于 doend 之间。

case 1 do
  2 -> 3
  4 -> 5
end

cond do
  true -> false
end

出现在类型规范中,位于 () 之间。

(integer(), boolean() -> integer())

它也用于 fnend 之间,用于构建匿名函数。

fn
  x, y -> x + y
end

标识符

标识符以 ~ 开头,后面跟着一个小写字母或一个或多个大写字母,紧跟着以下其中一对:

  • ()
  • {}
  • []
  • <>
  • ""
  • ''
  • ||
  • //

在闭合这对字符后,可以添加零个或多个 ASCII 字母和数字作为修饰符。标识符表示为以 sigil_ 为前缀的非限定调用,其中第一个参数是标识符内容作为字符串,第二个参数是修饰符的整数列表。

如果标识符字母是大写,则不允许在标识符中进行插值,否则其内容可能是动态的。比较下面标识符的结果以获取更多信息。

~s/f#{"o"}o/
~S/f#{"o"}o/

标识符对于使用其自身转义规则编码文本很有用,例如正则表达式、日期时间等。

Elixir AST

Elixir 语法设计为可以轻松转换为抽象语法树 (AST)。Elixir 的 AST 是一个正则 Elixir 数据结构,由以下元素组成:

  • 原子 - 例如 :foo
  • 整数 - 例如 42
  • 浮点数 - 例如 13.1
  • 字符串 - 例如 "hello"
  • 列表 - 例如 [1, 2, 3]
  • 包含两个元素的元组 - 例如 {"hello", :world}
  • 包含三个元素的元组,表示调用或变量,如下一节所述。

Elixir AST 的构建块是调用,例如:

sum(1, 2, 3)

它表示为包含三个元素的元组:

{:sum, meta, [1, 2, 3]}

第一个元素是原子(或另一个元组),第二个元素是包含元数据的两个元素元组列表(例如行号),第三个元素是参数列表。

我们可以通过调用 quote 获取任何 Elixir 表达式的 AST。

quote do
  sum()
end
#=> {:sum, [], []}

变量也使用包含三个元素的元组以及列表和原子的组合来表示,例如:

quote do
  sum
end
#=> {:sum, [], Elixir}

您可以看到变量也用元组表示,只是第三个元素是表示变量上下文的原子。

在本节中,我们将探索许多 Elixir 语法结构及其 AST 表示。

运算符

运算符被视为非限定调用。

quote do
  1 + 2
end
#=> {:+, [], [1, 2]}

请注意,. 也是一个运算符。远程调用在 AST 中使用带两个参数的点,其中第二个参数始终是原子。

quote do
  foo.bar(1, 2, 3)
end
#=> {{:., [], [{:foo, [], Elixir}, :bar]}, [], [1, 2, 3]}

调用匿名函数在 AST 中使用带单个参数的点,反映了函数名从点的右侧“缺失”的事实。

quote do
  foo.(1, 2, 3)
end
#=> {{:., [], [{:foo, [], Elixir}]}, [], [1, 2, 3]}

别名

别名由 __aliases__ 调用表示,每个段用点作为参数分隔。

quote do
  Foo.Bar.Baz
end
#=> {:__aliases__, [], [:Foo, :Bar, :Baz]}

quote do
  __MODULE__.Bar.Baz
end
#=> {:__aliases__, [], [{:__MODULE__, [], Elixir}, :Bar, :Baz]}

除了第一个参数之外,所有参数都保证是原子。

数据结构

请记住,列表是文字,因此它们在 AST 中以自身形式表示。

quote do
  [1, 2, 3]
end
#=> [1, 2, 3]

元组有自己的表示,除了包含两个元素的元组,它们以自身形式表示。

quote do
  {1, 2}
end
#=> {1, 2}

quote do
  {1, 2, 3}
end
#=> {:{}, [], [1, 2, 3]}

二进制的表示类似于元组,只是它们用 :<<>> 而不是 :{} 标记。

quote do
  <<1, 2, 3>>
end
#=> {:<<>>, [], [1, 2, 3]}

同样适用于映射,其中对被视为包含两个元素的元组列表。

quote do
  %{1 => 2, 3 => 4}
end
#=> {:%{}, [], [{1, 2}, {3, 4}]}

代码块

代码块表示为 __block__ 调用,每行作为单独的参数。

quote do
  1
  2
  3
end
#=> {:__block__, [], [1, 2, 3]}

quote do 1; 2; 3; end
#=> {:__block__, [], [1, 2, 3]}

左右箭头

左右箭头 (->) 的表示类似于运算符,只是它们始终是列表的一部分,其左侧表示参数列表,右侧是表达式。

例如,在 casecond 中:

quote do
  case 1 do
    2 -> 3
    4 -> 5
  end
end
#=> {:case, [], [1, [do: [{:->, [], [[2], 3]}, {:->, [], [[4], 5]}]]]}

quote do
  cond do
    true -> false
  end
end
#=> {:cond, [], [[do: [{:->, [], [[true], false]}]]]}

() 之间:

quote do
  (1, 2 -> 3
   4, 5 -> 6)
end
#=> [{:->, [], [[1, 2], 3]}, {:->, [], [[4, 5], 6]}]

fnend 之间:

quote do
  fn
    1, 2 -> 3
    4, 5 -> 6
  end
end
#=> {:fn, [], [{:->, [], [[1, 2], 3]}, {:->, [], [[4, 5], 6]}]}

限定元组

限定元组 (foo.{bar, baz}) 由 {:., [], [expr, :{}]} 调用表示,其中 expr 表示点的左侧,参数表示大括号内的元素。这在 Elixir 中用于提供多重别名。

quote do
  Foo.{Bar, Baz}
end
#=> {{:., [], [{:__aliases__, [], [:Foo]}, :{}]}, [], [{:__aliases__, [], [:Bar]}, {:__aliases__, [], [:Baz]}]}

可选语法

以上所有结构都是 Elixir 语法的一部分,并在 Elixir AST 中有自己的表示。本节将讨论剩余的结构,它们是以上结构的替代表示。换句话说,以下结构可以在您的 Elixir 代码中以多种方式表示,并保持 AST 等效性。我们称之为“可选语法”。

要了解 Elixir 可选语法的简要介绍,请参阅此文档。下面我们将继续提供更完整的参考。

其他进制的整数和 Unicode 代码点

Elixir 允许整数包含 _ 来分隔数字,并提供方便的方法来表示其他进制的整数。

1_000_000
#=> 1000000

0xABCD
#=> 43981 (Hexadecimal base)

0o01234567
#=> 342391 (Octal base)

0b10101010
#=> 170 (Binary base)


#=> 233 (Unicode code point)

这些结构只存在于语法级别。上面所有示例在 AST 中都表示为它们的底层整数。

访问语法

访问语法表示为对 Access.get/2 的调用。

quote do
  opts[arg]
end
#=> {{:., [], [Access, :get]}, [], [{:opts, [], Elixir}, {:arg, [], Elixir}]}

可选括号

Elixir 在带有一个或多个参数的本地和远程调用中提供可选括号。

quote do
  sum 1, 2, 3
end
#=> {:sum, [], [1, 2, 3]}

上面与 sum(1, 2, 3) 在解析器中处理方式相同。您可以在所有至少有一个参数的调用中删除括号。

您还可以跳过限定调用中的括号,例如 Foo.bar 1, 2, 3。在调用匿名函数时需要括号,例如 f.(1, 2, 3)

在实践中,开发人员更喜欢在大多数调用中添加括号。它们主要跳过 Elixir 的控制流结构,例如 defmoduleifcase 等,以及某些 DSL 中。

关键字

Elixir 中的关键字是包含两个元素的元组列表,其中第一个元素是原子。使用基本结构,它们将表示为:

[{:foo, 1}, {:bar, 2}]

但是,Elixir 引入了一个语法糖,上面的关键字可以写成如下形式:

[foo: 1, bar: 2]

包含外来字符(例如空格)的原子必须用引号括起来。此规则也适用于关键字。

[{:"foo bar", 1}, {:"bar baz", 2}] == ["foo bar": 1, "bar baz": 2]

请记住,由于列表和包含两个元素的元组是引号文字,因此根据定义,关键字也是文字(事实上,包含两个元素的元组成为引号文字的唯一原因是为了支持关键字作为文字)。

为了使关键字语法有效,: 之前不能有任何空格(foo : 1 无效),并且必须紧随空格(foo:1 无效)。

关键字作为最后一个参数

Elixir 还支持一种语法,如果调用的最后一个参数是关键字列表,则可以省略方括号。这意味着:

if(condition, do: this, else: that)

与以下相同:

if(condition, [do: this, else: that])

这反过来又与以下相同:

if(condition, [{:do, this}, {:else, that}])

do-end 代码块

最后一个语法便利是 do-end 代码块。do-end 代码块等效于函数调用最后一个参数的关键字,其中代码块内容用括号括起来。例如:

if true do
  this
else
  that
end

与以下相同:

if(true, do: (this), else: (that))

我们在上一节中已经探讨过。

括号对于支持多个表达式很重要。这:

if true do
  this
  that
end

与以下相同:

if(true, do: (
  this
  that
))

do-end 代码块内,您可以引入其他关键字,例如上面 if 中使用的 else。在 do-end 之间支持的关键字是静态的,并且是:

  • after
  • catch
  • else
  • rescue

您可以看到它们在 receivetry 等结构中使用。