Elixir入门教程-基本类型

  1. 基本算术运算
  2. 布尔类型
  3. 原子
  4. 字符串
  5. 匿名函数
  6. (链接) 列表
  7. 元组
  8. 列表还是元组?

本章我们将学习更多Elixir的基本数据类型:整数、浮点数、布尔值、原子、字符串、列表和元组。一些基本类型数据如下:

1
2
3
4
5
6
7
8
iex> 1 # 整数
iex> 0x1F # 整数
iex> 1.0 # 浮点数
iex> true # 布尔值
iex> :atom # 原子 / 符号
iex> "elixir" # 字符串
iex> [1, 2, 3] # 列表
iex> {1, 2, 3} # 元组

基本算术运算

打开 iex 输入如下表达式:

1
2
3
4
5
6
iex> 1 + 2
3
iex> 5 * 5
25
iex> 10 / 2
5.0

我们注意到 10 / 2 返回了一个浮点数5.0而不是整数5。这是正常的。在Elixir里,运算符 / 总是返回浮点数。如果你想要做整数除法或者想得到除法的余数,你可以调用 div 和 rem 函数:

1
2
3
4
5
6
iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

注意!Elixir允许你在调用一个命名函数时候省略圆括号。这个特性使得当你写声明和控制流结构的时候由更加简洁的语法。

Elixir也支持快捷符号来输入二进制、八进制以及十六进制数字:

1
2
3
4
5
6
iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31

浮点数最少要求小数点后有一位数字,并且也支持用e符号表达式表示指数数字:

1
2
3
4
iex> 1.0
1.0
iex> 1.0e-10
1.0e-10

在Elixir里浮点数是64位双精度。

你可以调用 round 函数获得与给定的浮点数最接近的整数,或者调用 trunc 函数获得一个浮点数的整数部分。

1
2
3
4
iex> round(3.58)
4
iex> trunc(3.58)
3

识别函数

Elixir里的函数是通过它的名字和函数参数个数来区分的。函数的arity指的是函数的参数个数。从这一点上,我们将在整个文档里使用的函数的名称和它的参数个数来描述函数。round/1 确定函数的名字是round,参数个数是1,而 round/2 确定一个不同(其实是不存在的)的函数,相同的名字,参数个数是2。

布尔类型

Elixir支持 true 和 false 为布尔值。

1
2
3
4
iex> true
true
iex> true == false
false

Elixir提供很多断言函数来检查一个值的类型。例如,is_boolean/2 函数可以用来检查一个值是不是布尔类型。

注:Elixir的函数是通过函数名和参数个数(即arity)来区分的。因此,is_boolean/1 表示函数名是is_boolean而且有一个入参。is_boolean/2 (其实并不存在这个函数)表示有相同的函数名却有不同的arity的函数。

1
2
3
4
iex> is_boolean(true)
true
iex> is_boolean(1)
false

如果入参是一个整数、一个浮点数或者其他数字,你也可以分别用 is_integer/1,is_float/1或is_number/1来检查。

注:在任何时候,你可以在shell里输入 h() 来打印如何使用shell的信息。这个 h 帮助者也可以用来访问任何函数的文档。例如,输入 h is_integer/1 将打印 is_integer/1 函数的文档。它也可以运用在操作符和其他结构上(试一下 h ==/2)。

原子

原子是常量,它们名字就是它们自己的值。其他语言里叫这种数据类型为符号:

1
2
3
4
iex> :hello
:hello
iex> :hello == :world
false

布尔值 true 和 false 实际上是原子。

1
2
3
4
5
6
iex> true == :true
true
iex> is_atom(false)
true
iex> is_boolean(:false)
true

字符串

在Elixir里,字符串被包裹在两个双引号中,并且它们被用UTF-8来编码的。

1
2
iex> "hellö"
"hellö"

注意:如果你在Windows中,你的终端可能默认就不用UTF-8编码。你可以通过在输入IEx前运行chcp 56001来改变当前会话的编码。

Elixir也支持字符串插入:

1
2
iex> "hellö #{:world}"
"hellö world"

字符串内可以有换行符。你可以用转义字符引入它们。

1
2
3
4
5
iex> "hello
...> world"
"hello\nworld"
iex> "hello\nworld"
"hello\nworld"

你可以用IO模块内的函数IO.puts/1来打印一个字符串。

1
2
3
4
iex> IO.puts "hello\nworld"
hello
world
:ok

注意:IO.puts/1函数在打印字符串后返回原子 :ok 作为结果。

在Elixir内部字符串是用一组字节组成的二进制数据来表示的。

1
2
iex> is_binary("hellö")
true

我们也可以获得一个字符串内的字节数。

1
2
iex> byte_size("hellö")
6

我们注意到上述例子的字符串的字节数是6,即使它只有5个字符。这是因为字符 “ö” 用UTF-8来编码的时候占用了两个字节。我们可以通过用String.length/1函数来基于字符数获得字符串的实际长度。

1
2
iex> String.length("hellö")
5

String模块包含了一些操作以Unicode标准定义的字符串的函数。

1
2
iex> String.upcase("hellö")
"HELLÖ"

匿名函数

匿名函数可以在行内被创建,并且它是被关键字fn和end界定的函数:

1
2
3
4
5
6
7
8
9
10
iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> add.(1, 2)
3
iex> is_function(add)
true
iex> is_function(add, 2) #检查add是否是一个有两个参数的函数
true
iex> is_function(add, 1) #检查add是否是一个有一个参数的函数
false

函数在Elixir里是“一等公民”,这意味着它跟整数和字符串一样可以被当作参数传给其他函数。上述例子,我们把持有函数的变量add传给函数is_function/1,得到了正确的结果 true。我们也能够通过调用is_function/2来检查这个函数的参数个数。

注意:在变量和圆括号之间的 点 号(.)在调用一个匿名函数的时候是必须存在的。这个点号确定在调用匿名函数add和命名函数add/2的时候没有歧义。在这个意义上,Elixir使匿名函数和函数名之间有明显的区别。我们将在第八章来看看那些区别。

匿名函数都是闭包,因此它可以访问那些和函数定义在一个范围内的变量。让我们定义一个新的匿名函数,它使用我们前面已经定义的匿名函数的变量add:

1
2
3
4
iex> double = fn a -> add.(a, a) end
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> double.(2)
4

记住:一个变量在一个函数里被赋值不会影响它周围的环境:

1
2
3
4
5
6
iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42

(链接)列表

Elixir用方括号来指定一个若干值组成的列表。这些值可以是任何类型:

1
2
3
4
iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3

两个列表可以用 ++/2 和 - -/2 操作符来串联和相减:

1
2
3
4
iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]

在本教材中,我们将谈论很多关于列表的头部和尾部的事情。头部是一个列表的第一个元素而尾部则是剩下的部分。它们可以用函数 hd/1 和 tl/1 来获取。让我们将一个列表赋给一个变量然后获取它的头部和尾部:

1
2
3
4
5
iex> list = [1, 2, 3]
iex> hd(list)
1
iex> tl(list)
[2, 3]

尝试获取一个空列表的头部或尾部会发生一个错误:

1
2
iex> hd []
** (ArgumentError) argument error

有时候你创建一个列表,返回来的是一个用单引号括起来的值。比如:

1
2
3
4
iex> [11, 12, 13]
'\v\f\r'
iex> [104, 101, 108, 108, 111]
'hello'

当Elixir遇到一个由可打印的ASCII码组成的列表,Elixir将把它当作字符列表打印出来(字面上的字符列表)。当和Erlang代码交互的时候字符列表十分常见。任何时候,当你在Elixir里遇到一个值而你不能十分确定它的类型的时候,你可以用 i/1 函数来获得它的信息。

1
2
3
4
5
6
7
8
9
10
11
iex> i 'hello'
Term
'hello'
Data type
List
Description
...
Raw representation
[104, 101, 108, 108, 111]
Reference modules
List

记住:单引号括住的值和双引号括住的值在Elixir里是不想等的,因为它们表示不同的类型:

1
2
iex> 'hello' == "hello"
false

单引号的是字符列表,双引号的是字符串。我们将在“二进制,字符串和字符列表”章节中详细讨论它们。

元组

Elixir用大括号来定义元组。和列表一样,元组可以持有任何类型的值。

1
2
3
4
iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2

元组会连续将元素存储在内存中。这意味着通过索引访问一个元组的元素或获取元组的大小是一个快速的操作。索引从零开始:

1
2
3
4
5
6
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2

用函数 put_elem/3 放置一个元素到一个元组的指定的索引位置也是可能的:

1
2
3
4
5
6
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}

注意:put_elem/3 返回了一个新的元组。原来存储在tuple变量里的元组没有被修改,因为Elixir的数据类型是不可修改的。由于是不可修改的,Elixir代码更容易推导,因为你永远不用担心是否有特别的代码正在某处修改你的数据结构。

列表还是元组

列表和元组有何不同?

列表在内存中就如链接列表,意味着列表中的每一个元素持有它的值和指向后继元素的指针,如此一直到列表的尾部。我们称每一个值和指针对是一个 cons cell:

1
2
iex> list = [1 | [2 | [3 | []]]]
[1, 2, 3]

这意味着访问列表的长度是一个线性操作:我们需要遍历整改列表来计算它的长度。只要我们在列表前面加元素则更新列表是快速的:

1
2
iex> [0 | list]
[0, 1, 2, 3]

相反,元组在内存中连续存储。这意味着获取元组的长度和通过索引获取元素是快速的。然而,元组的修改或增加是昂贵的操作,因为它需要在内存中拷贝整个元组。

这些性能特性决定了两类数据结构的用法。元组的一种非常通用的使用场景是用它们从函数返回额外的信息。例如:File.read/1 是一个可以用来读取文件内容的函数,而它返回的是元组:

1
2
3
4
iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}

如果传给File.read/1的路径存在,它返回一个元组,第一个元素是原子 :ok,第一个元素是文件内容。否则,它返回一个由原子 :error 和错误描述组成的元组。

大多数时候,Elixir会引导你做正确的事情。例如,有一个函数 elem/2 用来访问元组元素的,而对于列表则没有对应的内建函数:

1
2
3
4
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"

当计算一个数据结构的元素个数的时候,Elixir也遵从一个简单的规则:如果这个操作是在常数时间内的(即是该值是预先计算的)则这个函数命名为szie;或者,如果这个操作是线性的(即是计算长度随着输入数据的增长而越来越慢)则这个函数命名为length。为便于记忆,“长度”和“线性”都是“l”字母开始的。

例如,我们目前已经用过4个计算函数:byte_size/1 (计算字符串的字节数),tuple_size/1 (计算元组大小),length/1 (计算列表长度)以及 String.length/1(计算字符串的字符个数)。也就是说,我们用 byte_size 来获取字符串的字节数是廉价操作,但是用 String.length 获取unicode字符的个数可能是昂贵的操作因为整个字符串需要被遍历。

Elixir也提供 Port,Reference和PID作为数据类型(它们通常用于进程间通信),当我们讨论进程的时候将快速浏览它们一下。接下来,让我们看看在我们基本类型上的一些基本操作。

原文链接: http://elixir-lang.org/getting-started/basic-types.html#identifying-functions