├── 1-intro.md ├── 10-enum-stream.md ├── 11-process.md ├── 12-io.md ├── 13-alias-req-imp.md ├── 14-mod-attr.md ├── 15-structs.md ├── 16-proto.md ├── 17-try-catch.md ├── 18-comprehensions.md ├── 19-sigils.md ├── 2-basic-types.md ├── 20-typespecs-behaviors.md ├── 21-erlang-lib.md ├── 22-next.md ├── 3-basic-ops.md ├── 4-pattern-matching.md ├── 5-case-cond-if.md ├── 6-bin-str-charlist.md ├── 7-keywords-map-dict.md ├── 8-modules.md ├── 9-recursion.md ├── README.md └── cheat.ex /1-intro.md: -------------------------------------------------------------------------------- 1 | 1-简介 2 | ====== 3 | 4 | 欢迎! 5 | 6 | > 7 | 本章是主要讲了各个平台上如何安装使用Elixir。由于本文主要关注Elixir的语言学习, 8 | 因此这个章节所讲的步骤或工具可能不是最新,请大家自行网上搜索。 9 | 10 | 本章将涵盖如何安装Elixir,并且学习使用交互式的Elixir Shell(称为IEx)。 11 | 12 | 使用本教程的需求: 13 | - Erlang - version 17.0 或更高 14 | - Elixir - 1.0.0 或更高 15 | 16 | 现在开始吧! 17 | 18 | >如果你发现本手册有错误,请帮忙开_issue_讨论或发_pull request_。 19 | 20 | ## 1.1-安装包 21 | 在各个平台上最方便的安装方式是相应平台的安装包。 22 | 如果没有,推荐使用precompiled package或者用源码编译安装。 23 | 24 | 注意Elixir需要Erlang 17.0或更高。下面介绍的方法基本上都会自动为你安装Erlang。 25 | 假如没有,请阅读下面安装Erlang的说明。 26 | 27 | ### Mac OS X 28 | - Homebrew 29 | - 升级Homebrew到最新 30 | - 执行:```brew install elixir``` 31 | - Macports 32 | - 执行:```sudo port install elixir``` 33 | 34 | ### Unix(或者类Unix) 35 | - Fedora 17或更新 36 | - 执行:```yum install elixir``` 37 | - Fedora 22或更新 38 | - 执行:```dnf install elixir``` 39 | - Arch Linux (社区repo) 40 | - 执行:```pacman -S elixir``` 41 | - openSUSE (and SLES 11 SP3+) 42 | - 添加Erlang devel repo: ```zypper ar -f obs://devel:languages:erlang/ erlang``` 43 | - 执行:```zypper in elixir``` 44 | - Gentoo 45 | - 执行:```emerge --ask dev-lang/elixir``` 46 | - FreeBSD 47 | - 使用ports: ```cd /usr/ports/lang/elixir && make install clean``` 48 | - 或使用pkg: ```pkg install elixir``` 49 | - Ubuntu 12.04和14.04,或Debian 7 50 | - 添加Erlang Solutions repo: ```wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb``` 51 | - 执行:```sudo apt-get update``` 52 | - 安装Erlang/OTP平台及相关程序:```sudo apt-get install esl-erlang``` 53 | - 安装Elixir:```sudo apt-get install elixir``` 54 | 55 | ### Windows 56 | - Web installer 57 | - [下载installer](https://s3.amazonaws.com/s3.hex.pm/elixir-websetup.exe) 58 | - 点下一步,下一步...直到完成 59 | - Chocolatey 60 | - ```cinst elixir ``` 61 | 62 | ## 1.3-使用预编译包 63 | Elixir为每一个release提供了预编译包(编译好并打包的程序,开箱即用)。 64 | 首先[安装Erlang](http://elixir-lang.org/install.html#installing-erlang), 65 | 然后在[这里](https://github.com/elixir-lang/elixir/releases/)下载最新的 66 | 预编译包(Precompiled.zip)。unzip,即可使用elixir程序和iex程序了。 67 | 当然为了方便起见,需要将一些路径加入环境变量。 68 | 69 | ## 1.4-从源码编译安装(Unix和MinGW) 70 | 首先[安装Erlang](http://elixir-lang.org/install.html#installing-erlang), 71 | 然后在[这里](https://github.com/elixir-lang/elixir/releases/)下载最新的源码, 72 | 自己使用make工具编译安装。 73 | 74 | >在Windows上编译安装请参考https://github.com/elixir-lang/elixir/wiki/Windows 75 | 76 | >附上加环境变量的命令 77 | ```sh 78 | $ export PATH="$PATH:/path/to/elixir/bin" 79 | ``` 80 | 81 | 如果你十分激进,可以直接选择编译安装github上的master分支: 82 | ```sh 83 | $ git clone https://github.com/elixir-lang/elixir.git 84 | $ cd elixir 85 | $ make clean test 86 | ``` 87 | 如果测试无法通过,可在[repo](https://github.com/elixir-lang/elixir)里开issue汇报。 88 | 89 | ## 1.5-安装Erlang 90 | 安装Elixir唯一的要求就是Erlang(V17.0+), 91 | 它可以很容易地使用 92 | [预编译包](https://www.erlang-solutions.com/downloads/download-erlang-otp)安装。 93 | 如果你想从源码安装,可以去[Erlang网站](http://www.erlang.org/download.html)找找, 94 | 参考[Riak文档](http://docs.basho.com/riak/1.3.0/tutorials/installation/Installing-Erlang/)。 95 | 安装好Erlang后,打开命令行(或命令窗口),输入```erl```,可以输出Erlang的版本信息: 96 | ``` 97 | Erlang/OTP 17 (erts-6) [64-bit] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false] 98 | ``` 99 | >安装好Erlang后,你需要手动添加环境变量或$PATH。 100 | 关于环境变量,参考[这里](http://en.wikipedia.org/wiki/Environment_variable)。 101 | 102 | 103 | ## 1.6-交互模式 104 | 安装好Elixir之后,你有了三个可执行文件:```iex```,```elixir```和```elixirc```。 105 | 如果你是用预编译包方式安装的,可以在解压后的bin目录下找到它们。 106 | 107 | 现在我们可以从```iex```开始了(或者是```iex.bat```,如果在Windows上)。 108 | 交互模式,就是可以向其中输入任何Elixir表达式或命令,然后直接看到表达式或命令的结果。 109 | 如以下所示: 110 | ```elixir 111 | Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help) 112 | 113 | iex> 40 + 2 114 | 42 115 | iex> "hello" <> " world" 116 | "hello world" 117 | ``` 118 | 对这种交互式命令行,相信熟悉ruby,python等动态语言的程序员一定不会陌生。 119 | 120 | >如果你用的是Windows,你可以使用```iex.bat --werl```,可以根据你的console获得更好的使用体验。 121 | 122 | ## 1.7-执行脚本 123 | 把表达式写进脚本文件,可以用```elixir```命令执行它。如: 124 | ```sh 125 | $ cat simple.exs 126 | IO.puts "Hello world 127 | from Elixir" 128 | 129 | $ elixir simple.exs 130 | Hello world 131 | from Elixir 132 | ``` 133 | 134 | 在以后的章节中,我们还会介绍如何编译Elixir程序,以及使用Mix这样的构建工具。 135 | -------------------------------------------------------------------------------- /10-enum-stream.md: -------------------------------------------------------------------------------- 1 | 10-枚举类型和流 2 | ================== 3 | [枚举对象](#101-%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B)
4 | [积极vs懒惰](#102-%E7%A7%AF%E6%9E%81vs%E6%87%92%E6%83%B0)
5 | [流](#103-%E6%B5%81)
6 | 7 | ## 10.1-枚举类型 8 | Elixir提供了枚举类型(enumerables)的概念,使用[Enum模块]()操作它们。我们已经介绍过两种枚举类型:列表和图。 9 | ``` 10 | iex> Enum.map([1, 2, 3], fn x -> x * 2 end) 11 | [2, 4, 6] 12 | iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end) 13 | [2, 12] 14 | ``` 15 | 16 | Enum模块为枚举类型提供了大量函数来变化,排序,分组,过滤和读取元素。 17 | Enum模块是开发者最常用的模块之一。 18 |
19 | 20 | Elixir还提供了范围(range): 21 | ``` 22 | iex> Enum.map(1..3, fn x -> x * 2 end) 23 | [2, 4, 6] 24 | iex> Enum.reduce(1..3, 0, &+/2) 25 | 6 26 | ``` 27 | 28 | 因为Enum模块在设计时为了适用于不同的数据类型,所以它的API被限制为多数据类型适用的函数。 29 | 为了实现某些操作,你可能需要针对某类型使用某特定的模块。 30 | 比如,如果你要在列表中某特定位置插入一个元素,要用[List模块](http://elixir-lang.org/docs/stable/elixir/List.html)中的List.insert_at/3函数。而向某些类型内插入数据是没意义的,比如范围。 31 | 32 | Enum中的函数是多态的,因为它们能处理不同的数据类型。 33 | 尤其是,模块中可以适用于不同数据类型的函数,它们是遵循了[Enumerable协议](http://elixir-lang.org/docs/stable/elixir/Enumerable.html)。 34 | 我们在后面章节中将讨论这个协议。下面将介绍一种特殊的枚举类型:流。 35 | 36 | ## 10.2-积极vs懒惰 37 | Enum模块中的所有函数都是**积极**的。多数函数接受一个枚举类型,并返回一个列表: 38 | ``` 39 | iex> odd? = &(rem(&1, 2) != 0) 40 | #Function<6.80484245/1 in :erl_eval.expr/5> 41 | iex> Enum.filter(1..3, odd?) 42 | [1, 3] 43 | ``` 44 | 45 | 这意味着当使用Enum进行多种操作时,每次操作都生成一个中间列表,直到得出最终结果: 46 | ``` 47 | iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum 48 | 7500000000 49 | ``` 50 | 51 | 上面例子是一个含有多个操作的管道。从一个范围开始,然后给每个元素乘以3。 52 | 该操作将会生成的中间结果是含有100000个元素的列表。 53 | 然后我们过滤掉所有偶数,产生又一个新中间结果:一个50000元素的列表。 54 | 最后求和,返回结果。 55 | >这个符号的用法似乎和F#中的不一样啊... 56 | 57 | 作为一个替代,[流模块](http://elixir-lang.org/docs/stable/elixir/Stream.html)提供了懒惰的实现: 58 | ``` 59 | iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum 60 | 7500000000 61 | ``` 62 | 63 | 与之前Enum的处理不同,流先创建了一系列的计算操作。然后仅当我们把它传递给Enum模块,它才会被调用。流这种方式适用于处理大量的(甚至是无限的)数据集合。 64 | 65 | ## 10.3-流 66 | 流是懒惰的,比起Enum来说。 67 | 分步分析一下上面的例子,你会发现流与Enum的区别: 68 | ``` 69 | iex> 1..100_000 |> Stream.map(&(&1 * 3)) 70 | #Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]> 71 | ``` 72 | 流操作返回的不是结果列表,而是一个数据类型---流,一个表示要对范围1..100000使用map操作的动作。 73 | 74 | 另外,当我们用管道连接多个流操作时: 75 | ``` 76 | iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) 77 | #Stream<[enum: 1..100000, funs: [...]]> 78 | ``` 79 | 80 | 流模块中的函数接受任何枚举类型为参数,返回一个流。 81 | 流模块还提供了创建流(甚至是无限操作的流)的函数。 82 | 例如,```Stream.cycle/1```可以用来创建一个流,它能无限周期性枚举所提供的参数(小心使用): 83 | ``` 84 | iex> stream = Stream.cycle([1, 2, 3]) 85 | #Function<15.16982430/2 in Stream.cycle/1> 86 | iex> Enum.take(stream, 10) 87 | [1, 2, 3, 1, 2, 3, 1, 2, 3, 1] 88 | ``` 89 | 90 | 另一方面,```Stream.unfold/2```函数可以生成给定的有限值: 91 | ``` 92 | iex> stream = Stream.unfold("hełło", &String.next_codepoint/1) 93 | #Function<39.75994740/2 in Stream.unfold/2> 94 | iex> Enum.take(stream, 3) 95 | ["h", "e", "ł"] 96 | ``` 97 | 98 | 另一个有趣的函数是```Stream.resource/3```,它可以用来包裹某资源,确保该资源在使用前打开,在用完后关闭(即使中途出现错误)。--类似C#中的use{}关键字。 99 | 比如,我们可以stream一个文件: 100 | ``` 101 | iex> stream = File.stream!("path/to/file") 102 | #Function<18.16982430/2 in Stream.resource/3> 103 | iex> Enum.take(stream, 10) 104 | ``` 105 | 106 | 这个例子读取了文件的前10行内容。流在处理大文件,或者慢速资源(如网络)时非常有用。 107 |
108 | 109 | 一开始Enum和流模块中函数的数量多到让人气馁。但你会慢慢地熟悉它们。 110 | 建议先熟悉Enum模块,然后因为应用而转去流模块中那些相应的,懒惰版的函数。 111 | -------------------------------------------------------------------------------- /11-process.md: -------------------------------------------------------------------------------- 1 | 11-进程 2 | ======= 3 | 4 | Elixir里所有代码都在进程中执行。进程彼此独立,并发执行,通过传递消息(message)进行沟通。 5 | 进程不仅仅是Elixir并发编程的基础,也是Elixir创建分布式、高容错程序的本质。 6 | 7 | Elixir的进程和传统操作系统中的进程不可混为一谈。 8 | Elixir的进程在CPU和内存使用上,是极度轻量级的(但不同于其它语言中的线程)。 9 | 正因如此,同时运行着数十万、百万个进程也并不是罕见的事。 10 | 11 | 本章将讲解如何派生新进程,以及在不同进程间发送和接受消息等基本知识。 12 | 13 | ## 进程派生(spawning) 14 | 15 | 派生(spawning)一个新进程的方法是使用自动导入(kernel模块)的```spawn/1```函数: 16 | 17 | ```elixir 18 | iex> spawn fn -> 1 + 2 end 19 | #PID<0.43.0> 20 | ``` 21 | 22 | 函数```spawn/1```接收一个_函数_作为参数,在其派生出的进程中执行这个函数。 23 | 24 | 注意,```spawn/1```返回一个PID(进程标识)。在这个时候,这个派生的进程很可能已经结束。 25 | 26 | 派生的进程执行完函数后便会结束: 27 | 28 | ```elixir 29 | iex> pid = spawn fn -> 1 + 2 end 30 | #PID<0.44.0> 31 | iex> Process.alive?(pid) 32 | false 33 | ``` 34 | 35 | >你可能会得到与例子中不一样的PID 36 | 37 | 用```self/0```函数获取当前进程的PID: 38 | ```elixir 39 | iex> self() 40 | #PID<0.41.0> 41 | iex> Process.alive?(self()) 42 | true 43 | ``` 44 | 45 | >注:上文调用```self/0```加了括号。 46 | 但是如前文所说,在不引起误解的情况下,可以省略括号而只写```self``` 47 | 48 | 可以发送和接收消息,让进程变得越来越有趣。 49 | 50 | ## 发送和接收(消息) 51 | 52 | 使用```send/2```函数发送消息,用```receive/1```接收消息: 53 | 54 | ```elixir 55 | iex> send self(), {:hello, "world"} 56 | {:hello, "world"} 57 | iex> receive do 58 | ...> {:hello, msg} -> msg 59 | ...> {:world, msg} -> "won't match" 60 | ...> end 61 | "world" 62 | ``` 63 | 64 | 当有消息被发给某进程,该消息就被存储在该进程的“邮箱”里。 65 | 语句块```receive/1```检查当前进程的邮箱,寻找匹配给定模式的消息。 66 | 函数```receive/1```支持卫兵语句(guards)及分支子句(clause)如```case/2```。 67 | 68 | 如果找不到匹配的消息,当前进程将一直等待,直到下一条信息到达。 69 | 70 | 可以给等待设置一个超时时间: 71 | ```elixir 72 | iex> receive do 73 | ...> {:hello, msg} -> msg 74 | ...> after 75 | ...> 1_000 -> "nothing after 1s" 76 | ...> end 77 | "nothing after 1s" 78 | ``` 79 | 80 | 超时时间设为0表示你知道当前邮箱内肯定有邮件存在,很自信,因此设了这个极短的超时时间。 81 | 82 | 把以上概念综合起来,演示进程间发送消息: 83 | ```elixir 84 | iex> parent = self() 85 | #PID<0.41.0> 86 | iex> spawn fn -> send(parent, {:hello, self()}) end 87 | #PID<0.48.0> 88 | iex> receive do 89 | ...> {:hello, pid} -> "Got hello from #{inspect pid}" 90 | ...> end 91 | "Got hello from #PID<0.48.0>" 92 | ``` 93 | 94 | 在shell中执行程序时,辅助函数```flush/0```很有用。它清空并打印进程邮箱中的所有消息: 95 | ```elixir 96 | iex> send self(), :hello 97 | :hello 98 | iex> flush() 99 | :hello 100 | :ok 101 | ``` 102 | 103 | ## 链接(links) 104 | 105 | Elixir中最常用的进程派生方式是通过调用函数```spawn_link/1```。 106 | 107 | 在举例子讲解```spawn_link/1```之前,来看看如果一个进程挂掉会发生什么: 108 | ```elixir 109 | iex> spawn fn -> raise "oops" end 110 | #PID<0.58.0> 111 | 112 | [error] Process #PID<0.58.00> raised an exception 113 | ** (RuntimeError) oops 114 | :erlang.apply/2 115 | ``` 116 | 117 | ...仅仅是打印了一些文字信息,而派生这个倒霉进程的父进程仍然全无知觉地继续运行。 118 | 这是因为进程是互不干扰的。 119 | 如果我们希望一个进程发生异常挂掉可以被另一个进程感知,我们需要链接它们。 120 | 121 | 这需要使用```spawn_link/1```函数来派生进程,例子: 122 | ```elixir 123 | iex> spawn_link fn -> raise "oops" end 124 | #PID<0.60.0> 125 | ** (EXIT from #PID<0.41.0>) an exception was raised: 126 | ** (RuntimeError) oops 127 | :erlang.apply/2 128 | ``` 129 | 130 | 当这个失败发生在shell中,shell会自动捕获这个异常并显示格式优雅的异常信息。 131 | 为了弄明白失败时真正会发生什么,让我们在一个脚本文件中使用```spawn_link/1```: 132 | 133 | ```elixir 134 | # spawn.exs 135 | spawn_link fn -> raise "oops" end 136 | 137 | receive do 138 | :hello -> "let's wait until the process fails" 139 | end 140 | ``` 141 | 142 | 执行: 143 | ```elixir 144 | $ elixir spawn.exs 145 | 146 | ** (EXIT from #PID<0.47.0>) an exception was raised: 147 | ** (RuntimeError) oops 148 | spawn.exs:1: anonymous fn/0 in :elixir_compiler_0.__FILE__/1 149 | ``` 150 | 151 | 这次,该进程在失败时把它的父进程也弄停止了,因为它们是链接的。 152 | 153 | 使用函数```spawn_link/1```是在派生时对父子进程进行链接, 154 | 你还可以手动对两个进程进行链接:使用函数```Process.link/1```。 155 | 我们推荐可以多看看[Process模块](http://elixir-lang.org/docs/stable/elixir/Process.html),里面包含很多常用的进程操作函数。 156 | 157 | 进程和链接在创建能高容错系统时扮演重要角色。 158 | 在Elixir程序中,我们经常把进程链接到“管理者(supervisors)”上。 159 | 由这个角色负责检测失败进程,并且创建新进程取代之。这是唯一可行的方式。 160 | 因为进程间独立,默认情况下不共享任何东西。一个进程失败了,不会影响其它进程的状态。 161 | 162 | 其它语言通常需要我们抛出/捕获异常,而在Elixir中我们可以放任进程挂掉, 163 | 因为我们希望“管理者”会以更合适的方式重启系统。 164 | “死快一点(failing fast)”是Elixir软件开发中的一个常见哲学。 165 | 166 | `spawn/1`和`spawn_link/1`是Elixir中创建进程的基本方式。 167 | 尽管我们到目前为止都是专门调用它们,实际上大部分时间我们会使用基于它们功能的一些抽象操作。 168 | 比如常见的:“任务(Tasks)”。 169 | 170 | ## 任务(Task) 171 | 172 | 任务建立在进程派生函数之上,提供了更好的错误报告和内省。 173 | 174 | ```elixir 175 | iex(1)> Task.start fn -> raise "oops" end 176 | {:ok, #PID<0.55.0>} 177 | 178 | 15:22:33.046 [error] Task #PID<0.55.0> started from #PID<0.53.0> terminating 179 | ** (RuntimeError) oops 180 | (elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2 181 | (stdlib) proc_lib.erl:239: :proc_lib.init_p_do_apply/3 182 | Function: #Function<20.90072148/0 in :erl_eval.expr/5> 183 | Args: [] 184 | ``` 185 | 186 | 我们使用```Task.start/1```和```Task.start_link/1```代替```spawn/1```和```spawn_link/1```, 187 | 返回```{:ok pid}```而不仅仅是子进程的PID。这使得任务可以在监督者树中使用。 188 | 另外,任务提供了许多便利的函数,比如```Task.async/1```和```Task.await/1```等, 189 | 以及其它一些易于构建分布式接结构的函数。 190 | 191 | 我们将在《高级》篇中介绍关于任务更多的函数功能。现在我们只需要知道,使用任务有更好的错误报告。 192 | 193 | ## 状态(state) 194 | 195 | 目前为止我们还没有怎么谈到状态。但是如果你需要构建一个程序它需要状态,比如保存程序的配置信息, 196 | 或者解析一个文件先把它保存在内存里,你在哪儿存储? 197 | 198 | _进程_就是(最常见的一个)答案。我们可以写个进程执行无限循环,保存若干状态, 199 | 通过收发消息维护和改变这些状态值。 200 | 举个例子,我们写一个程序模块,创建一个提供键值对存储服务的进程: 201 | 202 | ```elixir 203 | defmodule KV do 204 | def start_link do 205 | Task.start_link(fn -> loop(%{}) end) 206 | end 207 | 208 | defp loop(map) do 209 | receive do 210 | {:get, key, caller} -> 211 | send caller, Map.get(map, key) 212 | loop(map) 213 | {:put, key, value} -> 214 | loop(Map.put(map, key, value)) 215 | end 216 | end 217 | end 218 | 219 | ``` 220 | 221 | 注意```start_link```函数启动一个新的进程。这个进程以一个空的图(map)为参数,执行```loop/1```函数。 222 | 启动后,```loop/1```函数等待消息,并且针对每个消息执行合适的操作。 223 | 如果收到```:get```消息,它会把消息发回给调用者,然后再次调用自身```loop/1```,等待新消息。 224 | 如果收到```:put```消息,它便用一个新版本的图变量(里面保存了新的键/值)再次调用```loop/1```。 225 | 226 | 执行一下```iex kv.exs```: 227 | ```elixir 228 | iex> {:ok, pid} = KV.start_link 229 | #PID<0.62.0> 230 | iex> send pid, {:get, :hello, self()} 231 | {:get, :hello, #PID<0.41.0>} 232 | iex> flush 233 | nil 234 | ``` 235 | 236 | 一开始进程内的图变量是没有键值的,所以发送一个```:get```消息并且刷新当前进程的收件箱,返回nil。 237 | 下面发送一个```:put```消息再试一次: 238 | 239 | ```elixir 240 | iex> send pid, {:put, :hello, :world} 241 | #PID<0.62.0> 242 | iex> send pid, {:get, :hello, self()} 243 | {:get, :hello, #PID<0.41.0>} 244 | iex> flush 245 | :world 246 | ``` 247 | 248 | 注意这个进程是怎么保持状态信息的:我们通过向该进程发送消息来获取和更新状态。 249 | 事实上,其它任何进程只要知道这个进程的PID,都能读取和修改状态。 250 | 251 | 还可以注册这个PID,给它一个名称。这使得我们可以通过名字来向它发送消息: 252 | ```elixir 253 | iex> Process.register(pid, :kv) 254 | true 255 | iex> send :kv, {:get, :hello, self()} 256 | {:get, :hello, #PID<0.41.0>} 257 | iex> flush 258 | :world 259 | ``` 260 | 261 | 使用进程维护状态、进程名称注册都是构建Elixir应用的常见模式。 262 | 但是大多数时间我们不会自己实现这样的模式,而是使用Elixir已经提供的抽象实现。 263 | 264 | 例如,Elixir提供的[agent](http://elixir-lang.org/docs/stable/elixir/Agent.html)就是一个状态维护进程的简单实现: 265 | 266 | ```elixir 267 | iex> {:ok, pid} = Agent.start_link(fn -> %{} end) 268 | {:ok, #PID<0.72.0>} 269 | iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end) 270 | :ok 271 | iex> Agent.get(pid, fn map -> Map.get(map, :hello) end) 272 | :world 273 | ``` 274 | 275 | 给```Agent.start_link/2```方法加上```:name```选项,可以自动为其注册一个名字。 276 | 277 | 除了agents,Elixir还提供了一套API来创建通用服务器(generic servers,称作GenServer),任务等。 278 | 这些都是建立在进程概念之上的实现。其它概念,包括“管理者”树,都可以在《高级》篇里找到更详细的说明。 279 | 280 | 下一章将介绍Elixir语言的I/O世界。 281 | -------------------------------------------------------------------------------- /12-io.md: -------------------------------------------------------------------------------- 1 | 12-IO和文件系统 2 | ====== 3 | 4 | 本章简单介绍Elixir的输入、输出机制,文件系统相关的任务, 5 | 以及涉及到的模块,如[`IO`](http://elixir-lang.org/docs/stable/elixir/IO.html), 6 | [`File`](http://elixir-lang.org/docs/stable/elixir/File.html) 7 | 和[`Path`](http://elixir-lang.org/docs/stable/elixir/Path.html)。 8 | 9 | 我们曾经在早期的文章中说现在介绍IO似乎有点早。 10 | 但是,我们注意到IO系统其实提供了一窥Elixir和虚拟机的设计哲学和精妙的绝佳机会。 11 | 12 | >“早期的文章”:现在介绍I/O似乎有点早,但是I/O系统可以让我们一窥Elixir哲学,满足我们对该语言以及VM的好奇心。 13 | 14 | ## `IO`模块 15 | 16 | 模块`IO`提供了Elixir语言中读写标准输入/输出(`:stdio`)、 17 | 标准错误(`:stderr`)、文件和其他IO设备的主要机制。 18 | 19 | 该模块的使用方法颇为直白: 20 | 21 | ```elixir 22 | iex> IO.puts "hello world" 23 | "hello world" 24 | :ok 25 | iex> IO.gets "yes or no? " 26 | yes or no? yes 27 | "yes\n" 28 | ``` 29 | 30 | 默认情况下,IO模块中的函数从标准输入中读取,向标准输出中写入。 31 | 我们可以传递参数,比如```:stderr```,来指示将信息写到标准错误上: 32 | 33 | ```elixir 34 | iex> IO.puts :stderr, "hello world" 35 | "hello world" 36 | :ok 37 | ``` 38 | 39 | ## `File`模块 40 | 41 | `File`模块包含的函数可以让我们打开文件作为IO设备。 42 | 默认情况下文件是以二进制模式打开, 43 | 它要求程序员使用特定的```IO.binread/2```和```IO.binwrite/2```函数来读写文件: 44 | 45 | ```elixir 46 | iex> {:ok, file} = File.open "hello", [:write] 47 | {:ok, #PID<0.47.0>} 48 | iex> IO.binwrite file, "world" 49 | :ok 50 | iex> File.close file 51 | :ok 52 | iex> File.read "hello" 53 | {:ok, "world"} 54 | ``` 55 | 56 | 文件可以使用`:utf8`编码方式打开,这让`File`模块以UTF-8编码方式解析文件中读取的字节: 57 | 58 | ```elixir 59 | iex> {:ok, file} = File.open "another", [:write, :utf8] 60 | {:ok, #PID<0.48.0>} 61 | ``` 62 | 63 | 除了打开、读写文件的函数,文件模块还提供了许多函数来操作文件系统。 64 | 这些函数根据Unix平台上功能相对应的命令来命名。 65 | 如`File.rm/1`用来删除文件,`File.mkdir/1`用来创建目录, 66 | `File.mkdir_p/1`创建目录并保证其父目录一并创建。 67 | 甚至还有`File.cp_r/2`和`File.rm_rf/1`用来递归地复制或删除整个目录。 68 | 69 | 你会注意到`File`模块的函数有两种变体,一个普通的和一个名称末尾有`!`(bang)的。 70 | 例如在上面的例子里,我们在读取“hello”文件时,用的是不带`!`的版本。 71 | 相对地,我们也可以使用`File.read!/1`: 72 | 73 | ```elixir 74 | iex> File.read "hello" 75 | {:ok, "world"} 76 | iex> File.read! "hello" 77 | "world" 78 | iex> File.read "unknown" 79 | {:error, :enoent} 80 | iex> File.read! "unknown" 81 | ** (File.Error) could not read file unknown: no such file or directory 82 | ``` 83 | 84 | 注意看,当文件不存在时,带!号的版本会抛出一个错误。 85 | 而不带!号的版本适用于你对不同结果进行模式匹配的情形: 86 | 87 | ```elixir 88 | case File.read(file) do 89 | {:ok, body} -> # do something with the `body` 90 | {:error, reason} -> # handle the error caused by `reason` 91 | end 92 | ``` 93 | 94 | 但有的时候,你就是希望文件在那儿,!号变体更加适用,因为它能报出有意义的错误。 95 | 例如,如果这么写: 96 | 97 | ```elixir 98 | {:ok, body} = File.read(file) 99 | ``` 100 | 101 | 当遇到文件不存在的情况时,函数返回`{:error, reason}`,然后导致在跟左侧元组做模式匹配时失败。 102 | 失败依然会抛出一个异常,但是该异常的错误信息是描述一次模式匹配失败,而不是文件的什么错误。 103 | 从而在一定程度上掩盖了真正的失败原因。 104 | 105 | 可以这么写: 106 | 107 | ```elixir 108 | case File.read(file) do 109 | {:ok, body} -> # handle ok 110 | {:error, r} -> # handle error 111 | end 112 | ``` 113 | 114 | 当然,更推荐的写法: 115 | 116 | ```elixir 117 | File.read!(file) 118 | ``` 119 | 120 | ## ```Path```模块 121 | 122 | 模块`File`中的绝大多数函数都以各种路径作为参数。 123 | 通常,这些路径都是二进制串(binaries)。 124 | 模块`Path`提供了操作这些路径的函数: 125 | 126 | ```elixir 127 | iex> Path.join("foo", "bar") 128 | "foo/bar" 129 | iex> Path.expand("~/hello") 130 | "/Users/jose/hello" 131 | ``` 132 | 133 | 推荐使用`Path`模块提供的函数而不是直接手动操作代表路径的二进制串。 134 | 因为`Path`模块考虑了不同操作系统的区别,使得各种处理是对“操作系统”透明的。 135 | 最后,记住在Windows平台上处理文件操作时,Elixir会自动将斜杠(/)转换成反斜杠(\)。 136 | 137 | 有了上面介绍的模块和函数,我们已经能对文件系统进行基本的操作。 138 | 下面将讨论有关IO的一些高级话题。这部分并不是写Elixir程序必须掌握的,可以跳过不看。 139 | 但是如果你浏览一下,可以大致了解IO是如何在VM上实现的,以及其它一些有趣的内容。 140 | 141 | ## 进程(Processes)和组长(group leaders) 142 | 143 | 你可能已经注意到`File.open/2`函数返回类似`{:ok, pid}`的元组: 144 | 145 | ```elixir 146 | iex> {:ok, file} = File.open "hello", [:write] 147 | {:ok, #PID<0.47.0>} 148 | ``` 149 | 150 | 模块`IO`实际上是和进程(Process)一起协同工作的。 151 | 当你调用`IO.write(pid, binary)`时,`IO`模块将发送一条消息给`pid`标示的进程, 152 | 附上所期望进行的操作。 153 | 154 | 如果我们自己用进程来描述这个过程: 155 | 156 | ```elixir 157 | iex> pid = spawn fn -> 158 | ...> receive do: (msg -> IO.inspect msg) 159 | ...> end 160 | #PID<0.57.0> 161 | iex> IO.write(pid, "hello") 162 | {:io_request, #PID<0.41.0>, #Reference<0.0.8.91>, {:put_chars, :unicode, "hello"}} 163 | ** (ErlangError) erlang error: :terminated 164 | ``` 165 | 166 | 调用`IO.write/2`之后,可以看到`IO`模块发出的请求(四个元素的元组)被打印了出来。 167 | 不久后,因为我们并未提供`IO`模块期待的某种结果,这个请求失败了。 168 | 169 | [`StringIO`模块](http://elixir-lang.org/docs/stable/elixir/StringIO.html) 170 | 提供了基于字符串的`IO`设备消息处理功能(将字符串看做IO设备): 171 | 172 | ```elixir 173 | iex> {:ok, pid} = StringIO.open("hello") 174 | {:ok, #PID<0.43.0>} 175 | iex> IO.read(pid, 2) 176 | "he" 177 | ``` 178 | 179 | Erlang虚拟机通过利用进程给IO设备建模,使得同一个网络中的不同节点可以通过交换文件进程, 180 | 实现跨节点的文件读写。而在所有IO设备之中,有一个特殊的进程,称作*组长(group leader)*。 181 | 182 | 当你写东西到标准输入输出(`:stdio`),实际上是发送了一条消息给进程组长, 183 | 它把内容写给标准输出的文件描述符: 184 | 185 | ```elixir 186 | iex> IO.puts :stdio, "hello" 187 | hello 188 | :ok 189 | iex> IO.puts Process.group_leader, "hello" 190 | hello 191 | :ok 192 | ``` 193 | 194 | 在不同的应用场景下,让哪个进程作为组长是可以配置的。 195 | 例如,当在远程终端执行代码时,通过配置组长,可以使得远程节点上打印的消息可以被重定向到发起操作(你)的终端上。 196 | 197 | ## `iodata`和`chardata` 198 | 199 | 在以上所有例子中,我们都用的是二进制串格式写入文件。 200 | 在“二进制串、字符串和字符列表”那章里,我们提到过字符串(string)就是普通的bytes, 201 | 而字符列表(char list)就是字符编码(code point,如“Uxxxx”、“049”)的列表。 202 | 203 | `IO`模块和`File`模块中的函数还可以接受列表作为参数。 204 | 不光如此,它们还接受混合类型的列表,里面内容可以是列表(如`'ab c'`)、 205 | 整形(如`49`,`?A` --- 返回65)和二进制串(如`"ab c"`): 206 | 207 | ```elixir 208 | iex> IO.puts 'hello world' 209 | hello world 210 | :ok 211 | iex> IO.puts ['hello', ?\s, "world"] 212 | hello world 213 | :ok 214 | ``` 215 | 216 | >`?`返回后面字符的编码(code point),如`?A`默认情况下返回65。 217 | 218 | 尽管如此,有些地方还是要注意。一个列表可能表示一串byte,或者一串字符。 219 | 用哪一种需要看IO设备是怎么编码的。 220 | 如果不指明编码,文件就以raw模式打开, 221 | 这时候只能用`IO`模块里`bin*`开头(二进制版本)的函数对其进行操作。 222 | 这些函数接受`iodata`作为参数。也就是说,它们期待一个整数值的列表,用来表示bytes或二进制串。 223 | 224 | 另一边,使用`:utf8`打开的`:stdio`和文件使用`IO`模块里剩下来其它的函数。 225 | 这些函数期待`char_data`作为参数,即一串字符或字符串的列表。 226 | 227 | 尽管差别比较精妙,但只是当你想要传递列表给那些函数的时候,才用担心一下细节问题。 228 | 而二进制串(binaries)表示了底层的bytes字节列表,这种表示已经是raw的了。 229 | -------------------------------------------------------------------------------- /13-alias-req-imp.md: -------------------------------------------------------------------------------- 1 | 13-alias,require和import 2 | ================= 3 | 4 | 为了实现软件重用,Elixir提供了三种指令(`alias`,`require`和`import`), 5 | 外加一个宏命令`use`,如下: 6 | 7 | ```elixir 8 | # 给模块起别名,让它可以用 Bar 调用而非 Foo.Bar 9 | alias Foo.Bar, as: Bar 10 | 11 | # 确保模块已被编译且可用(通常为了宏) 12 | require Foo 13 | 14 | # 从 Foo 中导入函数,使之调用时不用加`Foo`前缀 15 | import Foo 16 | 17 | # 执行定义在 Foo 拓展点内的代码 18 | use Foo 19 | ``` 20 | 21 | 下面我们将深入细节。记住前三个之所以称之为“指令”, 22 | 是因为它们的作用域是*词法作用域(lexicla scope)*, 23 | 而`use`是一个普通拓展点(common extension point),可以将宏展开。 24 | 25 | ## alias 26 | 27 | 指令`alias`可以为任何模块设置别名。 28 | 想象一下之前使用过的`Math`模块,它针对特殊的数学运算提供了特殊的列表(list)实现: 29 | 30 | ```elixir 31 | defmodule Math do 32 | alias Math.List, as: List 33 | end 34 | ``` 35 | 36 | 现在,任何对`List`的引用将被自动变成对`Math.List`的引用。 37 | 如果还想访问原来的`List`,可以加上它的模块名前缀'Elixir': 38 | 39 | ```elixir 40 | List.flatten #=> uses Math.List.flatten 41 | Elixir.List.flatten #=> uses List.flatten 42 | Elixir.Math.List.flatten #=> uses Math.List.flatten 43 | ``` 44 | 45 | >注意:Elixir中定义的所有模块都被定义在Elixir命名空间内。 46 | 但为方便起见,在引用它们时,你可以省略它们的前缀‘Elixir’。 47 | 48 | 别名常被使用于定义快捷方式。实际应用中,不带`:as`选项调用`alias`会 49 | 自动将别名设置为该模块名称的最后一部分: 50 | 51 | ```elixir 52 | alias Math.List 53 | ``` 54 | 55 | 就相当于: 56 | 57 | ```elixir 58 | alias Math.List, as: List 59 | ``` 60 | 61 | 注意,`alias`是**词法作用域**。也就是说,当你在某个函数中设置别名: 62 | 63 | ```elixir 64 | defmodule Math do 65 | def plus(a, b) do 66 | alias Math.List 67 | # ... 68 | end 69 | 70 | def minus(a, b) do 71 | # ... 72 | end 73 | end 74 | ``` 75 | 76 | 例子中`alias`指令设置的别名只在函数`plus/2`中有效,函数`minus/2`则不受影响。 77 | 78 | ## require 79 | 80 | Elixir提供了许多宏用于元编程(可以编写生成代码的代码)。 81 | 82 | 宏是在编译时被执行和展开的代码。 83 | 也就是说为了使用宏,你需要确保定义这个宏的模块及实现在你的代码的编译时可用(即被加载)。 84 | 这使用`require`指令实现: 85 | 86 | ```elixir 87 | iex> Integer.odd?(3) 88 | ** (CompileError) iex:1: you must require Integer before invoking the macro Integer.odd?/1 89 | iex> require Integer 90 | nil 91 | iex> Integer.odd?(3) 92 | true 93 | ``` 94 | 95 | Elixir中,`Integer.odd?/1`函数被定义为一个宏,因此它可以被当作卫兵表达式(guards)使用。 96 | 为了调用这个宏,首先需要使用`require`引用`Integer`模块。 97 | 98 | 总的来说,一个模块在被用到之前不需要早早地require,除非我们需要用到这个模块中定义的宏的时候。 99 | 尝试调用一个没有加载的宏时,会报出一个异常。 100 | 注意,像`alias`指令一样,`require`指令也是词法作用域的。 101 | 在后面章节我们会进一步讨论宏。 102 | 103 | ## import 104 | 105 | 当想轻松地访问模块中的函数和宏时,可以使用`import`指令避免输入模块的完整名字。 106 | 例如,如果我们想多次使用`List`模块中的`duplicate/2`函数,我们可以import它: 107 | 108 | ```elixir 109 | iex> import List, only: [duplicate: 2] 110 | List 111 | iex> duplicate :ok, 3 112 | [:ok, :ok, :ok] 113 | ``` 114 | 115 | 这个例子中,我们只从List模块导入了函数`duplicate`(元数是2的那个)。 116 | 尽管`:only`选项是可选的,但是仍推荐使用,以避免向当前命名空间内导入这个模块内定义的所有函数。 117 | 还有`:except`选项,可以*排除*一些函数而导入其余的。 118 | 119 | 还有选项`:only`,传递给它`:macros`或`:functions`,来导入该模块的所有宏或函数。 120 | 如下面例子,程序仅导入`Integer`模块中定义的所有的宏: 121 | 122 | ```elixir 123 | import Integer, only: :macros 124 | ``` 125 | 126 | 或者,仅导入所有的函数: 127 | 128 | ```elixir 129 | import Integer, only: :functions 130 | ``` 131 | 132 | 注意,`import`也遵循**词法作用域**,意味着我们可以在某特定函数定义内导入宏或方法: 133 | 134 | ```elixir 135 | defmodule Math do 136 | def some_function do 137 | import List, only: [duplicate: 2] 138 | duplicate(:ok, 10) 139 | end 140 | end 141 | ``` 142 | 143 | 在这个例子中,导入的函数`List.duplicate/2`只在函数`some_function`中可见, 144 | 在该模块的其它函数中都不可用(自然,别的模块也不受影响)。 145 | 146 | 注意,若`import`一个模块,将自动`require`它。 147 | 148 | ## use 149 | 150 | 尽管不是一条指令,`use`是一个宏,与帮助你在当前上下文中使用模块的`require`指令联系紧密。 151 | `use`宏常被用来引入外部的功能到当前的词法作用域---通常是模块。 152 | 153 | 例如,在编写测试时,我们使用ExUnit框架。开发者需要使用`ExUnit.Case` 模块: 154 | 155 | ```elixir 156 | defmodule AssertionTest do 157 | use ExUnit.Case, async: true 158 | 159 | test "always pass" do 160 | assert true 161 | end 162 | end 163 | ``` 164 | 165 | 在代码背后,`use`宏先是`require`所给的模块,然后在模块上调用`__using__/1`回调函数, 166 | 从而允许这个模块在当前上下文中注入某些代码。 167 | 168 | 比如下面这个模块: 169 | 170 | ```exlixir 171 | defmodule Example do 172 | use Feature, option: :value 173 | end 174 | ``` 175 | 176 | 会被编译成(即宏`use`扩展) 177 | 178 | ```exlixir 179 | defmodule Example do 180 | require Feature 181 | Feature.__using__(option: :value) 182 | end 183 | ``` 184 | 185 | 到这里,关于Elixir的模块基本上讲得差不多了。后面会讲解模块的属性(Attribute)。 186 | 187 | ## 别名机制 188 | 189 | 讲到这里你会问,Elixir的别名到底是什么,它是怎么实现的? 190 | 191 | Elixir中的别名是以大写字母开头的标识符(像`String`, `Keyword`),在编译时会被转换为原子。 192 | 例如,别名‘String’默认情况下会被转换为原子`:"Elixir.String"`: 193 | 194 | ```elixir 195 | iex> is_atom(String) 196 | true 197 | iex> to_string(String) 198 | "Elixir.String" 199 | iex> :"Elixir.String" == String 200 | true 201 | ``` 202 | 203 | 使用`alias/2`指令,其实只是简单地改变了这个别名将要转换的结果。 204 | 205 | 别名会被转换为原子,是因为在Erlang虚拟机(以及上面的Elixir)中,模块是由原子表述。 206 | 例如,我们调用一个Erlang模块函数的机制是: 207 | 208 | ```elixir 209 | iex> :lists.flatten([1,[2],3]) 210 | [1, 2, 3] 211 | ``` 212 | 213 | 这也是允许我们动态调用模块函数的机制: 214 | 215 | ```elixir 216 | iex> mod = :lists 217 | :lists 218 | iex> mod.flatten([1,[2],3]) 219 | [1,2,3] 220 | ``` 221 | 222 | 我们只是简单地在原子`:lists`上调用了函数`flatten`。 223 | 224 | ## 模块嵌套 225 | 226 | 我们已经介绍了别名,现在可以讲讲嵌套(nesting)以及它在Elixir中是如何工作的。 227 | 考虑下面的例子: 228 | 229 | ```elixir 230 | defmodule Foo do 231 | defmodule Bar do 232 | end 233 | end 234 | ``` 235 | 236 | 该例子定义了两个模块:`Foo`和`Foo.Bar`。 237 | 后者在`Foo`中可以用`Bar`为名来访问,因为它们在同一个词法作用域中。 238 | 上面的代码等同于: 239 | 240 | ```elixir 241 | defmodule Elixir.Foo do 242 | defmodule Elixir.Foo.Bar do 243 | end 244 | alias Elixir.Foo.Bar, as: Bar 245 | end 246 | ``` 247 | 248 | 如果之后开发者决定把`Bar`模块定义挪出`Foo`模块的定义,但是在`Foo`中仍然使用`Bar`来引用, 249 | 那它就需要以全名(Foo.Bar)来命名,或者向上文提到的,在`Foo`中设置个别名来指代。 250 | 251 | **注意:** 在Elixir中,你并不是必须在定义`Foo.Bar`模块之前定义`Foo`模块, 252 | 因为编程语言会将所有模块名翻译成原子。 253 | 你可以定义任意嵌套的模块而不需要注意其名称链上的先后顺序 254 | (比如,在定义`Foo.Bar.Baz`前不需要提前定义`foo`或者`Foo.Bar`)。 255 | 256 | 在后面几章我们会看到,别名在宏里面也扮演着重要角色,来保证它们是“干净”(hygienic)的。 257 | 258 | ## 一次、多个 259 | 260 | 从Elixir v1.2版本开始,可以一次性使用alias,import,require操作多个模块。 261 | 这在定义和使用嵌套模块的时候非常有用,这也是在构建Elixir程序的常见情形。 262 | 263 | 例如,假设你的程序所有模块都嵌套在`MyApp`下, 264 | 你可以一次同时给三个模块:`MyApp.Foo`,`MyApp.Bar`和`MyApp.Baz`提供别名: 265 | 266 | ```elixir 267 | alias MyApp.{Foo, Bar, Baz} 268 | ``` 269 | -------------------------------------------------------------------------------- /14-mod-attr.md: -------------------------------------------------------------------------------- 1 | 14-模块属性 2 | =========== 3 | 4 | 在Elixir中,模块属性(module attributes)主要服务于三个目的: 5 | 1. 作为一个模块的注解(annotations),通常附加上用户或虚拟机会用到的信息 6 | 2. 作为常量 7 | 3. 在编译时作为一个临时的模块存储机制 8 | 9 | 下面让我们来一一讲解。 10 | 11 | ## 作为注解(annotations) 12 | 13 | Elixir从Erlang带来了模块属性的概念。如: 14 | 15 | ```elixir 16 | defmodule MyServer do 17 | @vsn 2 18 | end 19 | ``` 20 | 21 | 这个例子中,我们显式地为该模块设置了 _版本(vsn即version)_ 属性。 22 | `@vsn`是一个系统保留的属性名称,它被Erlang虚拟机的代码装载机制使用,以检查该模块是否被更新过。 23 | 如果不注明版本号,该属性的值会自动设置为模块函数的md5 checksum。 24 | 25 | Elixir还有好多系统保留的预定义注解。下面是一些比较常用的: 26 | 27 | * `@moduledoc` - 为当前模块提供文档说明 28 | * `@doc` - 为该属性标注的函数或宏提供文档说明 29 | * `@behaviour` - (注意这个单词是英式拼法)用来注明一个OTP或用户自定义行为 30 | * `@before_compile` - 提供一个每当模块被编译之前执行的钩子。这使得我们可以在模块被编译之前往里面注入函数 31 | 32 | `@moduledoc`和`@doc`是目前最常用的注解属性,我们也希望你能够多使用它们。 33 | Elixir视文档为一等公民,而且提供了很多方法来访问这些文档。 34 | 你可以拓展阅读文章[《用我们官方的方式写Elixir程序文档》](http://elixir-lang.org/docs/stable/elixir/writing-documentation.html)。 35 | 36 | 让我们回到上几章定义的`Math`模块,为它添加文档,然后依然保存在math.ex文件中: 37 | 38 | ```elixir 39 | defmodule Math do 40 | @moduledoc """ 41 | Provides math-related functions. 42 | 43 | ## Examples 44 | 45 | iex> Math.sum(1, 2) 46 | 3 47 | 48 | """ 49 | 50 | @doc """ 51 | Calculates the sum of two numbers. 52 | """ 53 | def sum(a, b), do: a + b 54 | end 55 | ``` 56 | 57 | Elixir推荐使用markdown语法和多行文本(heredocs)书写容易阅读的文档。 58 | heredocs是多行的字符串,用三个双引号包裹,它会保持里面内容的格式不变。 59 | 我们可以在IEx中读取任何编译的模块的文档: 60 | 61 | ```elixir 62 | $ elixirc math.ex 63 | $ iex 64 | ``` 65 | 66 | ``` 67 | iex> h Math # Access the docs for the module Math 68 | ... 69 | iex> h Math.sum # Access the docs for the sum function 70 | ... 71 | ``` 72 | 73 | Elixir还提供了[ExDoc工具](https://github.com/elixir-lang/ex_doc), 74 | 利用注释生成HTML页文档。 75 | 76 | 你可以看看[模块](http://elixir-lang.org/docs/stable/elixir/Module.html) 77 | 里面列出的完整的模块注解列表,Elixir还利用注解来定义[typespecs](../20-typespecs-behaviors.md)。 78 | 79 | 本节讲了一些内置的注解。当然,注解可以被开发者和类库扩展使用,来支持自定义的行为。 80 | 81 | ## 作为常量 82 | 83 | Elixir开发者经常会将模块属性当作常量使用: 84 | 85 | ```elixir 86 | defmodule MyServer do 87 | @initial_state %{host: "147.0.0.1", port: 3456} 88 | IO.inspect @initial_state 89 | end 90 | ``` 91 | 92 | >不同于Erlang,默认情况下用户定义的属性不会被存储在模块里。属性值仅在编译时存在。 93 | 开发者可以通过调用`Module.register_attribute/3`来使这种属性的行为更接近Erlang。 94 | 95 | 访问一个未定义的属性会报警告: 96 | 97 | ```elixir 98 | defmodule MyServer do 99 | @unknown 100 | end 101 | warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it to nil before access 102 | ``` 103 | 104 | 最后,属性也可以在函数中被读取: 105 | 106 | ```elixir 107 | defmodule MyServer do 108 | @my_data 14 109 | def first_data, do: @my_data 110 | @my_data 13 111 | def second_data, do: @my_data 112 | end 113 | 114 | MyServer.first_data #=> 14 115 | MyServer.second_data #=> 13 116 | ``` 117 | 118 | 注意,在函数内读取某属性,读取的是该属性值的一份快照。换句话说,读取的是编译时的值,而非运行时。 119 | 后面我们将看到,这个特点使得属性可以作为模块在编译时的临时存储,十分有用。 120 | 121 | ## 作为临时存储 122 | 123 | Elixir组织中有一个项目,叫做[Plug](https://github.com/elixir-lang/plug), 124 | 这个项目的目标是创建一个通用的Web库和框架。 125 | 126 | >注:我想功能应该类似于ruby的rack。你可以定义各种plug,这这些plug会像链条一样, 127 | 按顺序各自对http请求进行加工处理,最后返回。这类似给rails或sinatra定义各种rack中间件, 128 | 也有些类似Java filter的概念。最终,Plug框架会组织和执行它们。 129 | 130 | Plug库允许开发者定义它们自己的plug,运行在web服务器上: 131 | 132 | ```elixir 133 | defmodule MyPlug do 134 | use Plug.Builder 135 | 136 | plug :set_header 137 | plug :send_ok 138 | 139 | def set_header(conn, _opts) do 140 | put_resp_header(conn, "x-header", "set") 141 | end 142 | 143 | def send_ok(conn, _opts) do 144 | send(conn, 200, "ok") 145 | end 146 | end 147 | 148 | IO.puts "Running MyPlug with Cowboy on http://localhost:4000" 149 | Plug.Adapters.Cowboy.http MyPlug, [] 150 | ``` 151 | 152 | 上面例子中,我们用了`plug/1`宏来连接处理请求时会被调用的函数。 153 | 在代码背后,每次调用宏`plug/1`时,Plug库把提供的参数(即plug的名字)存储在`@plugs`属性里。 154 | 就在模块被编译之前,Plug会执行一个回调函数,该回调函数定义一个函数(`call/2`)来处理http请求。 155 | 这个函数将按顺序执行所有保存在`@plugs`属性里的plugs。 156 | 157 | 要理解底层的代码,我们还需要了解宏,因此我们将在后期《元编程》章节中回顾这个模式。 158 | 这里的重点是怎样使用属性来存储数据,让开发者可以创建DSL(领域特定语言)。 159 | 160 | 另一个例子来自[ExUnit框架](http://elixir-lang.org/docs/stable/ex_unit/), 161 | 它使用模块属性作为注解和存储: 162 | 163 | ```elixir 164 | defmodule MyTest do 165 | use ExUnit.Case 166 | 167 | @tag :external 168 | test "contacts external service" do 169 | # ... 170 | end 171 | end 172 | ``` 173 | 174 | ExUnit中,标签(Tag)被用来注解该测试用例。在标记之后,这些标签可以用来过滤测试用例。 175 | 例如,你可以避免执行那些被标记成`:external`的测试,因为它们执行起来很慢而且可以依赖外部的东西。 176 | 但是它们依然在你的工程之内。 177 | 178 | 本章带你一窥Elixir元编程的冰山一角,讲解了模块属性在开发中是如何扮演关键角色的。 179 | 下一章将讲解结构体(structs)和协议(protocols),在前进到其它更远的知识点(诸如异常处理等)之前。 180 | -------------------------------------------------------------------------------- /15-structs.md: -------------------------------------------------------------------------------- 1 | 15-结构体 2 | ========= 3 | 4 | 在之前的某章中,我们学习了图(Map): 5 | 6 | ```elixir 7 | iex> map = %{a: 1, b: 2} 8 | %{a: 1, b: 2} 9 | iex> map[:a] 10 | 1 11 | iex> %{map | a: 3} 12 | %{a: 3, b: 2} 13 | ``` 14 | 15 | 结构体是基于图的一个扩展。它引入了默认值、编译期验证。 16 | 17 | 定义一个结构体,只需调用`defstruct/1`: 18 | 19 | ```elixir 20 | iex> defmodule User do 21 | ...> defstruct name: "john", age: 27 22 | ...> end 23 | 24 | ``` 25 | 26 | `defstruct/1`的参数(一个键值列表)定义了结构体的字段和默认值。结构体使用了定义它的模块的名字。 27 | 像上面这个例子,我们定义的结构体叫做`User`。 28 | 29 | 现在可以用类似创建图的语法来创建结构体`User`: 30 | 31 | ```elixir 32 | iex> %User{} 33 | %User{age: 27, name: "John"} 34 | iex> %User{name: "Meg"} 35 | %User{age: 27, name: "Meg"} 36 | ``` 37 | 38 | 结构体提供*编译期验证*,在代码在编译时会检查结构体内仅有之前定义的字段(而且一个字段也不少): 39 | 40 | ```elixir 41 | iex> %User{ oops: :field } 42 | ** (CompileError) iex:3: unknown key :oops for struct User 43 | ``` 44 | 45 | ## 访问和修改结构体 46 | 47 | 当讨论图的时候,我们演示了如何访问和修改图的字段。 48 | 访问和修改结构体的技术(以及语法)也是一样的: 49 | 50 | ```elixir 51 | iex> john = %User{} 52 | %User{age: 27, name: "John"} 53 | iex> john.name 54 | "John" 55 | iex> meg = %{john | name: "Meg"} 56 | %User{age: 27, name: "Meg"} 57 | iex> %{meg | oops: :field} 58 | ** (KeyError) key :oops not found in: %User{age: 27, name: "Meg"} 59 | ``` 60 | 61 | 当使用语法标记(`|`)的时候,虚拟机知道并没有增加新的字段,这使得底层的图可以共享内存中得结构。 62 | 像上文的例子,`john`和`meg`在内存中使用相同的键值结构。 63 | 64 | 结构体也能用在模式匹配中,这不但可以保证结构体的字段名称的值相同,也可以确保对应的字段值的类型也相同: 65 | 66 | ```elixir 67 | iex> %User{name: name} = john 68 | %User{age: 27, name: "John"} 69 | iex> name 70 | "John" 71 | iex> %User{} = %{} 72 | ** (MatchError) no match of right hand side value: %{} 73 | ``` 74 | 75 | ## 结构体和底层的图 76 | 77 | 在上面的例子里,之所以可以用模式匹配,是因为结构体底层不过是拥有固定字段的图。 78 | 而作为图,结构体还存储了一个名叫`__struct__`的特殊字段,来存储结构体的名字: 79 | 80 | ```elixir 81 | iex> is_map(john) 82 | true 83 | iex> john.__struct__ 84 | User 85 | ``` 86 | 87 | 简单说,结构体就是个被扒光的图外加一个默认字段。 88 | 为啥说是被扒光的图?因为,所有为图实现的协议(protocols)都不能用于结构体。 89 | 例如,你不能像对图那样枚举或直接访问一个结构体: 90 | 91 | ```elixir 92 | iex> john = %User{} 93 | %User{age: 27, name: "John"} 94 | iex> john[:name] 95 | ** (UndefinedFunctionError) undefined function: User.fetch/2 96 | iex> Enum.each john, fn({field, value}) -> IO.puts(value) end 97 | ** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"} 98 | ``` 99 | 100 | 尽管如此,因为结构体说到底还是图,对图有效的函数也可以作用于结构体: 101 | 102 | ```elixir 103 | iex> kurt = Map.put(%User{}, :name, "Kurt") 104 | %User{age: 27, name: "Kurt"} 105 | iex> Map.merge(kurt, %User{name: "Takashi"}) 106 | %User{age: 27, name: "Takashi"} 107 | iex> Map.keys(john) 108 | [:__struct__, :age, :name] 109 | ``` 110 | 111 | 结构体和协议(protocols),为Elixir程序员提供了一个最重要的特性:数据多态。我们会在下一章解释和学习。 112 | 113 | 下一章我们将介绍结构体是如何同协议进行交互的。 114 | -------------------------------------------------------------------------------- /16-proto.md: -------------------------------------------------------------------------------- 1 | 16-协议(protocols) 2 | ======== 3 | 4 | 协议是实现Elixir多态性的重要机制。任何数据类型只要实现了某协议, 5 | 那么基于该协议的(函数调用)消息分发就是可用的。 6 | 7 | >先简单解释一下上面“分发(dispatching)”的意思:对于许多编程语言, 8 | 特别是支持“duck-typing”的语言来说,对象调用方法,相当于以该对象为目的, 9 | 对其发送消息(函数/方法名),希望它支持该方法调用。 10 | 这里的“协议”二字对于熟悉ruby等具有“duck-typing”特性的语言的人来说会比较容易理解。 11 | 12 | 让我们看个例子。 13 | 14 | 在Elixir中,只有`false`和`nil`被认为是“false”的。其它的值都被认为是“true”。 15 | 根据程序需要,有时需要一个`blank?`协议(注意,我们此处称之为“协议”), 16 | 返回一个布尔值,以说明该参数是否为空。 17 | 举例来说,一个空列表或者空二进制可以被认为是空的。 18 | 19 | 我们可以如下定义协议: 20 | ```elixir 21 | defprotocol Blank do 22 | @doc "Returns true if data is considered blank/empty" 23 | def blank?(data) 24 | end 25 | ``` 26 | 27 | 从上面代码的语法上看,这个协议`Blank`声明了一个函数`blank?`,接受一个参数。 28 | 我们可以为不同的数据类型实现这个协议: 29 | 30 | ```elixir 31 | # 整型永远不为空 32 | defimpl Blank, for: Integer do 33 | def blank?(_), do: false 34 | end 35 | 36 | # 只有空列表是“空”的 37 | defimpl Blank, for: List do 38 | def blank?([]), do: true 39 | def blank?(_), do: false 40 | end 41 | 42 | # 只有空map是“空” 43 | defimpl Blank, for: Map do 44 | # 一定要记住,我们不能匹配 %{} ,因为它能match所有的map。 45 | # 但是我们能检查它的size是不是0 46 | # 检查size是很快速的操作 47 | def blank?(map), do: map_size(map) == 0 48 | end 49 | 50 | # 只有false和nil这两个原子被认为是“空” 51 | defimpl Blank, for: Atom do 52 | def blank?(false), do: true 53 | def blank?(nil), do: true 54 | def blank?(_), do: false 55 | end 56 | ``` 57 | 58 | 我们可以为所有内建数据类型实现协议: 59 | - 原子 60 | - 比特串 61 | - 浮点型 62 | - 函数 63 | - 整型 64 | - 列表 65 | - 图 66 | - PID 67 | - Port 68 | - 引用 69 | - 元组 70 | 71 | 现在手上有了一个协议的定义以及其实现,可如此使用之: 72 | 73 | ```elixir 74 | iex> Blank.blank?(0) 75 | false 76 | iex> Blank.blank?([]) 77 | true 78 | iex> Blank.blank?([1, 2, 3]) 79 | false 80 | ``` 81 | 82 | 给它传递一个并没有实现该协议的数据类型,会导致报错: 83 | 84 | ```elixir 85 | iex> Blank.blank?("hello") 86 | ** (Protocol.UndefinedError) protocol Blank not implemented for "hello" 87 | ``` 88 | 89 | ## 协议和结构体 90 | 91 | Elixir的可扩展性(extensiblility)来源于将协议和结构体一同使用。 92 | 93 | 在前面几章中我们知道,尽管结构体本质上就是图(map),但是它和图并不共享各自协议的实现。 94 | 像那章一样,我们先定义一个名为`User`的结构体: 95 | 96 | ```elixir 97 | iex> defmodule User do 98 | ...> defstruct name: "john", age: 27 99 | ...> end 100 | {:module, User, <<70, 79, 82, ...>>, {:__struct__, 0}} 101 | ``` 102 | 103 | 然后看看能不能用刚才定义的协议: 104 | 105 | ```elixir 106 | iex> Blank.blank?(%{}) 107 | true 108 | iex> Blank.blank?(%User{}) 109 | ** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"} 110 | ``` 111 | 112 | 果然,结构体没有使用这个协议针对图的实现。 113 | 因此,对于这个结构体,需要定义它的协议实现: 114 | 115 | ```elixir 116 | defimpl Blank, for: User do 117 | def blank?(_), do: false 118 | end 119 | ``` 120 | 121 | 如果愿意,你可以定义你自己的语法来检查一个user是否为空。 122 | 不光如此,你还可以使用结构体创建更强健的数据类型(比如队列),然后实现所有相关的协议, 123 | 比如`Enumerable`,或者是`Blank`等等。 124 | 125 | ## 实现`Any` 126 | 127 | 手动给所有类型实现某些协议实现很快就会变得犹如重复性劳动般枯燥无味。 128 | 在这种情况下,Elixir给出两种选择:一是显式让我们的类型继承某些已有的实现; 129 | 二是,自动给所有类型提供实现。这两种情况,我们都需要为`Any`类型写实现代码。 130 | 131 | ### 继承 132 | 133 | Elixir允许我们继承某些有基于`Any`类型的实现。比如,我们先为`Any`实现某个协议: 134 | 135 | ```Elixir 136 | defimpl Blank, for: Any do 137 | def blank?(_), do: false 138 | end 139 | ``` 140 | 141 | OK我们现在有个协议通用的实现了。在定义结构体时,可以显式标注其继承了`Blank`协议的实现: 142 | 143 | ```Elixir 144 | defmodule DeriveUser do 145 | @derive Blank 146 | defstruct name: "john", age: 27 147 | end 148 | ``` 149 | 150 | 继承的时候,Elixir会为`DeriveUser`实现`Blank`协议(基于`Blank`在`Any`上的实现)。 151 | 这种方式是可选择的:结构体只会跟它们自己显式实现或继承实现的协议一起工作。 152 | 153 | ### 退化至`Any` 154 | 155 | `@derive`注解的一个替代物是显式地告诉协议,如果没有找到(在某个类型上得)实现的时候, 156 | 使用其`Any`的实现(如果有)。在定义协议的时候设置`@fallback_to_any`为`true`即可: 157 | 158 | ```elixir 159 | defprotocol Blank do 160 | @fallback_to_any true 161 | def blank?(data) 162 | end 163 | ``` 164 | 165 | 假使我们在前一小节已经完成了对`Any`的实现: 166 | 167 | ```elixir 168 | defimpl Blank, for: Any do 169 | def blank?(_), do: false 170 | end 171 | ``` 172 | 173 | 现在,所有没有实现`blank`协议的数据类型(包括结构体)都会被认为是非空的。 174 | 对比`@derive`,退化至`Any`是必然的:只有没有显式提供某个实现的实现,所有类型对于某个协议,都会有默认的行为。 175 | 这项技术提供了很大的灵活性,也支持了Elixir程序员“显式先于隐式”的编码哲学。 176 | 你可以在很多库中看到`@derive`的身影。 177 | 178 | ## 内建协议 179 | 180 | Elixir内建了一些协议。在前面几章中我们讨论过`Enum`模块,它提供了许多方法。 181 | 只要任何一种数据结构它实现了`Enumerable`协议,就能使用这些方法: 182 | 183 | ```elixir 184 | iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end 185 | [2,4,6] 186 | iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end 187 | 6 188 | ``` 189 | 190 | 另一个例子是`String.Chars`协议,它规定了如何将包含字符的数据结构转换为字符串类型。 191 | 它暴露为函数```to_string```: 192 | 193 | ```elixir 194 | iex> to_string :hello 195 | "hello" 196 | ``` 197 | 198 | 注意,在Elixir中,字符串插值操作背后就调用了```to_string```函数: 199 | 200 | ```elixir 201 | iex> "age: #{25}" 202 | "age: 25" 203 | ``` 204 | 205 | 上面代码能工作,是因为25这个数字类型实现了`String.Chars`协议。 206 | 而如果传进去的是元组就会报错: 207 | 208 | ```elixir 209 | iex> tuple = {1, 2, 3} 210 | {1, 2, 3} 211 | iex> "tuple: #{tuple}" 212 | ** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3} 213 | ``` 214 | 215 | 当想要打印一个比较复杂的数据结构时,可以使用`inspect`函数。该函数基于协议`Inspect`: 216 | 217 | ```elixir 218 | iex> "tuple: #{inspect tuple}" 219 | "tuple: {1, 2, 3}" 220 | ``` 221 | 222 | `Inspect`协议用来将任意数据类型转换为可读的文字表述。IEx用来打印表达式结果用的就是它: 223 | 224 | ```elixir 225 | iex> {1, 2, 3} 226 | {1,2,3} 227 | iex> %User{} 228 | %User{name: "john", age: 27} 229 | ``` 230 | 231 | >```inspect```是ruby中非常常用的方法。 232 | 这也能看出Elixir的作者们真是绞尽脑汁把Elixir的语法尽量往ruby上靠。 233 | 234 | 记住,被执行`inspect`函数后的结果,是头顶着`#`符号的Elixir的类型描述文本,本身并不是合法的Elixir语法。 235 | 在转换为可读的文本后,数值丢失了信息,因此别指望还能从该字符串取回原来的那个东西: 236 | 237 | ```elixir 238 | iex> inspect &(&1+2) 239 | "#Function<6.71889879/1 in :erl_eval.expr/5>" 240 | ``` 241 | 242 | Elixir中还有些其它协议,但本章就讲这几个比较常用的。 243 | 244 | ## 协议压实(consolidation) 245 | 246 | 当使用Mix构建工具的时候,你可能会看到如下输出: 247 | 248 | ``` 249 | Consolidated String.Chars 250 | Consolidated Collectable 251 | Consolidated List.Chars 252 | Consolidated IEx.Info 253 | Consolidated Enumerable 254 | Consolidated Inspect 255 | ``` 256 | 257 | 这些都是Elixir内建的协议,它们正在被“压实”(压紧、夯实;还有更好的翻译么?)。 258 | 因为协议可以被分发给所有的数据类型,在每一次调用时,协议必须检查是否对某数据类型提供了实现。 259 | 这消耗大量资源。 260 | 261 | 但是,如果使用构建工具Mix,我们会知道所有的模块已被定义,包括协议和实现。 262 | 这样,协议可以被优化到一个简单的易于快速分发的模块中。 263 | 264 | 从Elixir v1.2开始,这种优化是自动进行的。后面的《Mix和OTP教程》中会讲到。 265 | -------------------------------------------------------------------------------- /17-try-catch.md: -------------------------------------------------------------------------------- 1 | 17-异常处理 2 | =========== 3 | 4 | Elixir有三种错误处理机制:errors,throws和exits。 5 | 本章我们将逐个讲解它们,包括应该在何时使用哪一个。 6 | 7 | ## Errors 8 | 9 | 错误(errors,或者叫它异常)用在代码中出现意外的地方。 10 | 举个例子,尝试让原子加上一个数字,就会返回一个错误: 11 | 12 | ```elixir 13 | iex> :foo + 1 14 | ** (ArithmeticError) bad argument in arithmetic expression 15 | :erlang.+(:foo, 1) 16 | ``` 17 | 18 | `raise/1`可以在任何时候激发一个运行时错误: 19 | 20 | ```elixir 21 | iex> raise "oops" 22 | ** (RuntimeError) oops 23 | ``` 24 | 25 | 也可以调用`raise/2`抛出错误,并且附上错误名称和一个键值列表: 26 | 27 | ```elixir 28 | iex> raise ArgumentError, message: "invalid argument foo" 29 | ** (ArgumentError) invalid argument foo 30 | ``` 31 | 32 | 你可以定义一个模块,在里面使用`defexception/2`定义你自己的错误。 33 | 这种方式,你创建的错误和模块同名。最常见的是定义一个有详细错误信息字段的错误: 34 | 35 | ```elixir 36 | iex> defmodule MyError do 37 | iex> defexception message: "default message" 38 | iex> end 39 | iex> raise MyError 40 | ** (MyError) default message 41 | iex> raise MyError, message: "custom message" 42 | ** (MyError) custom message 43 | ``` 44 | 45 | 错误可以用`try/catch`结构 **拯救(rescued)** : 46 | 47 | ```elixir 48 | iex> try do 49 | ...> raise "oops" 50 | ...> rescue 51 | ...> e in RuntimeError -> e 52 | ...> end 53 | %RuntimeError{message: "oops"} 54 | ``` 55 | 56 | 这个例子处理了一个运行时异常,返回该错误本身(被显示在`:iex`回话中)。 57 | 58 | 如果你不需要使用错误对象,你可以不提供它: 59 | 60 | ```elixir 61 | iex> try do 62 | ...> raise "oops" 63 | ...> rescue 64 | ...> RuntimeError -> "Error!" 65 | ...> end 66 | "Error!" 67 | ``` 68 | 69 | 在实际应用中,Elixir程序员极少使用`try/rescue`结构。 70 | 例如,当文件打开失败,很多编程语言会强制你去处理异常。 71 | 而Elixir提供的`File.read/1`函数不管文件打开成功与否,都会返回包含结果信息的元组: 72 | 73 | ```elixir 74 | iex> File.read "hello" 75 | {:error, :enoent} 76 | iex> File.write "hello", "world" 77 | :ok 78 | iex> File.read "hello" 79 | {:ok, "world"} 80 | ``` 81 | 82 | 这个例子中没有用到`try/rescue`。 83 | 如果你想处理打开文件可能发生的不同结果,你可以使用`case`结构来做模式匹配: 84 | 85 | ```elixir 86 | iex> case File.read "hello" do 87 | ...> {:ok, body} -> IO.puts "Success: #{body}" 88 | ...> {:error, reason} -> IO.puts "Error: #{reason}" 89 | ...> end 90 | ``` 91 | 92 | 使用这种匹配处理,你就可以自己决定打开文件异常是不是一种错误。 93 | 这也是为什么Elixir不让`File.read/1`等函数自己抛出异常。 94 | 它把决定权留给程序员,让他们寻找最合适的处理方法。 95 | 96 | 如果你真的期待文件存在(_打开文件时文件不存在_ 确实是一个 **错误**), 97 | 你可以简单地使用`File.read!/1`: 98 | 99 | ```elixir 100 | iex> File.read! "unknown" 101 | ** (File.Error) could not read file unknown: no such file or directory 102 | (elixir) lib/file.ex:305: File.read!/1 103 | ``` 104 | 105 | 但是标准库中的许多函数都遵循这样的模式:不返回元组来模式匹配,而是抛出异常。 106 | 具体说,这种约定是在定义名字不带感叹号后缀的函数时,返回结果信息的元组; 107 | 定义带有感叹号后缀的相同函数时,使用触发异常的机制。 108 | 可以阅读`File`模块的代码,有很多关于这种约定的好例子。 109 | 110 | 总之在Elixir中,我们避免使用`try/rescue`是因为我们**不通过错误处理机制来控制程序执行流程**。 111 | 我们视错误为其字面意思:它们只不过是用来处理意外时用到的信息。 112 | 如果你真的希望异常机制改变程序执行流程,可以使用`throws`。 113 | 114 | ## Throws 115 | 116 | 在Elixir中,你可以抛出(throw)一个值(不一定是一个错误/异常对象)做后续处理并终止当前其它操作。 117 | (前方文字高能=>)`throw`和`catch`就被保留用来处理这样有值被抛出、但是不用`try/catch`就取不到的情况。 118 | 119 | >译注:这句话原文也差不多。仔细读读感觉逻辑类似于“X被用来描述不用X就无法描述的东西”。 120 | 121 | 这中情况很不多,非要找得话,比如当一个库的接口没有提供合适的API时。 122 | 举个例子,`Enum`模块没有提供现成的API来做这个奇葩的事情:寻找若干数字里第一个13的倍数。 123 | 你可以利用`throw`机制,在刚找到第一个符合条件的数时,抛出这个数字,并终止执行流程: 124 | 125 | ```elixir 126 | iex> try do 127 | ...> Enum.each -50..50, fn(x) -> 128 | ...> if rem(x, 13) == 0, do: throw(x) 129 | ...> end 130 | ...> "Got nothing" 131 | ...> catch 132 | ...> x -> "Got #{x}" 133 | ...> end 134 | "Got -39" 135 | ``` 136 | 137 | >但实际上`Enum`提供了这样的API---使用`Enum.find/2`: 138 | 139 | >```elixir 140 | iex> Enum.find -50..50, &(rem(&1, 13) == 0) 141 | -39 142 | ``` 143 | 144 | ## Exits 145 | 146 | Elixir代码都在各种进程中运行,彼此间相互通信。当一个进程因为自然原因死亡了(如“未处理的异常), 147 | 它会发出`exit`信号。一个进程可以通过显式地发出`exit`信号来终止: 148 | 149 | ```elixir 150 | iex> spawn_link fn -> exit(1) end 151 | #PID<0.56.0> 152 | ** (EXIT from #PID<0.56.0>) 1 153 | ``` 154 | 155 | 上面的例子中,被链接的进程通过发送`exit`信号(带有参数1)而终止。 156 | Elixir shell自动处理这个消息并把它们显示在终端上。 157 | 158 | >打比方,在某进程中执行的Elixir代码,突遇故障难以处理,便大喊一声“要死”后死亡; 159 | 或者它主动喊出一声“要死”而死亡。是该进程发送消息(即,调用函数)。 160 | 161 | `exit`可以被`try/catch`块捕获处理: 162 | 163 | ```elixir 164 | iex> try do 165 | ...> exit "I am exiting" 166 | ...> catch 167 | ...> :exit, _ -> "not really" 168 | ...> end 169 | "not really" 170 | ``` 171 | 172 | `try/catch`已经很少使用了,用它们捕获`exit`信号就更少见了。 173 | 174 | `exit`信号是Erlang虚拟机提供的高容错性的重要部分。 175 | 进程通常都在*监督树(supervision trees)*下运行。 176 | 监督树本身也是进程,它任务就是一直等待下面被监督进程的`exit`信号。 177 | 一旦监听到退出信号,监督策略就开始工作,发送了死亡信号的被监督进程会被重启。 178 | 179 | 就是这种监督机制使得`try/catch`和`try/rescue`代码块很少用到。 180 | 与其拯救一个错误,不如让它*快速失败*。 181 | 因为在程序发生失败后,监督树会保证这个程序恢复到一个已知的初始状态去。 182 | 183 | ## After 184 | 185 | 有时候我们有必要确保某资源在执行可能会发生错误的操作后被正确地关闭或清理。 186 | 使用`try/after`结构来做这个。 187 | 例如打开一个文件,使用`after`子句来确保它在使用后被关闭,即使发生了错误: 188 | 189 | ```elixir 190 | iex> {:ok, file} = File.open "sample", [:utf8, :write] 191 | iex> try do 192 | ...> IO.write file, "olá" 193 | ...> raise "oops, something went wrong" 194 | ...> after 195 | ...> File.close(file) 196 | ...> end 197 | ** (RuntimeError) oops, something went wrong 198 | ``` 199 | 200 | `after`块中的代码,无论`try`代码块中是否出现错误,其都会执行。 201 | 但是,也有例外。如果一个链接进程整个终止了,它代码中的`after`块可能还没有执行。 202 | 因此,我们说`after`只是个“软”保障。 203 | 幸运的是,在Elixir语言中,打开的文件永远是链接到当前进程的。如果当前进程挂了,文件也会跟着关闭。 204 | 不管执没执行`after`块。 205 | 其它一些资源,如ETS表,套接字,端口等也是这样。 206 | 207 | 有时候你想将整个函数体用`try`结构包起来,是某些代码在某情况下会后续执行。 208 | 这时,Elixir允许你不写`try`那行: 209 | 210 | ```elixir 211 | iex> defmodule RunAfter do 212 | ...> def without_even_trying do 213 | ...> raise "oops" 214 | ...> after 215 | ...> IO.puts "cleaning up!" 216 | ...> end 217 | ...> end 218 | iex> RunAfter.without_even_trying 219 | cleaning up! 220 | ** (RuntimeError) oops 221 | ``` 222 | 223 | 这时,Elixir会将整个函数体的代码用`try`块包裹,不管后面是`after`,`rescue`还是`catch`。 224 | 225 | ## 变量的作用域 226 | 227 | 定义在`try/catch/rescue/after`代码块中的变量,不会泄露到外面去。 228 | 这是因为`try`代码块有可能会失败,而这些变量此时并没有正常绑定数值: 229 | 230 | 换句话说,下面这份代码是错误的: 231 | 232 | ```elixir 233 | iex> try do 234 | ...> raise "fail" 235 | ...> what_happened = :did_not_raise 236 | ...> rescue 237 | ...> _ -> what_happened = :rescued 238 | ...> end 239 | iex> what_happened 240 | ** (RuntimeError) undefined function: what_happened/0 241 | ``` 242 | 243 | 相反,你可以保存`try`表达式的返回值: 244 | 245 | ```elixir 246 | iex> what_happened = 247 | ...> try do 248 | ...> raise "fail" 249 | ...> :did_not_raise 250 | ...> rescue 251 | ...> _ -> :rescued 252 | ...> end 253 | iex> what_happened 254 | :rescued 255 | ``` 256 | 257 | 至此我们结束了对`try/catch/rescue`的介绍。 258 | 你会发现Elixir语言中的这些概念的使用频率比其他语言小,尽管的确有时使用起来也挺顺手。 259 | -------------------------------------------------------------------------------- /18-comprehensions.md: -------------------------------------------------------------------------------- 1 | 18-速构(Comprehension) 2 | ======== 3 | 4 | > *Comprehensions* 翻译成“速构”是参照了《Erlang/OTP in Action》译者的用辞。 5 | 国内一些Python书(是的,Python也有这个概念)中翻译为“推导式”、“递推式”。 6 | 这里不用纠结它的翻译,更重要的是需要弄明白它是什么。 7 | 8 | >“速构”是函数式语言中常见的概念,指的是定义规则来生成一系列元素填充新的数据集合。 9 | 这个概念我们在中学的数学课上其实就已经接触过,在大学高数中更为常见: 10 | 如`{ x | x ∈ N }`这个表达式,字面上意思是:这是一个集合, 11 | 这个集合里每个元素x符合“x属于自然数N”这个条件。即,用自然数集合的所有元素来构成这个集合。 12 | 相关知识可参考[WIKI](http://en.wikipedia.org/wiki/List_comprehension)。 13 | 14 | Elixir中,使用枚举类型(Enumerable,如列表)来做循环操作是很常见的, 15 | 通常还搭配过滤(filtering)和映射(mapping)行为。 16 | 速构(comprehensions)就是为此目的诞生的语法糖:把这些常见任务分组,放到特殊的`for`指令中表达出来。 17 | 18 | 例如,我们可以这样,生成原列表中每个元素的平方: 19 | 20 | ```elixir 21 | iex> for n <- [1, 2, 3, 4], do: n * n 22 | [1, 4, 9, 16] 23 | ``` 24 | 25 | >注意看,`<-`符号其实是模拟符号`∈ `的形象。 26 | 这个例子用熟悉(当然,如果你高数课没怎么听那就另当别论)的数学符号表示就是: 27 | 28 | ``` 29 | S = { X^2 | X ∈ [1,4], X ∈ N } 30 | ``` 31 | 32 | 速构由三部分组成:生成器,过滤器和收集式。 33 | 34 | ## 生成器和过滤器 35 | 36 | 在上面的例子中,`n <- [1, 2, 3, 4]`就是生成器。 37 | 它字面意思上生成了即将要在速构中使用的数值。任何枚举类型(Enumerable)都可以传递给生成器表达式的右端: 38 | 39 | ```elixir 40 | iex> for n <- 1..4, do: n * n 41 | [1, 4, 9, 16] 42 | ``` 43 | 44 | 生成器表达式左操作数支持模式匹配,它会**忽略**所有不匹配的模式。 45 | 想象一下如果不用范围而是用一个键值列表作为生成器的数据源,它的键只有`:good`和`:bad`两种, 46 | 我们仅要计算键为‘:good’的元素值的平方: 47 | 48 | ```elixir 49 | iex> values = [good: 1, good: 2, bad: 3, good: 4] 50 | iex> for {:good, n} <- values, do: n * n 51 | [1, 4, 16] 52 | ``` 53 | 54 | 除了使用模式匹配,过滤器也可以用来选择某些特定数值。 55 | 例如我们可以只选择3的倍数,而丢弃其它数值: 56 | 57 | ```elixir 58 | iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end 59 | iex> for n <- 0..5, multiple_of_3?.(n), do: n * n 60 | [0, 9] 61 | ``` 62 | 63 | 速构过程会丢弃过滤器表达式结果为`false`或`nil`的值;其它值都会被保留。 64 | 65 | 总的来说,速构提供了比直接使用`Enum`或`Stream`模块的函数更精确的表达。 66 | 不但如此,速构还可以接受多个生成器和过滤器。下面就是一个例子,代码接受目录列表, 67 | 删除这些目录下的所有文件: 68 | 69 | ```elixir 70 | for dir <- dirs, 71 | file <- File.ls!(dir), 72 | path = Path.join(dir, file), 73 | File.regular?(path) do 74 | File.stat!(path).size 75 | end 76 | ``` 77 | 78 | 多生成器还可以用来生成两个列表的笛卡尔积: 79 | 80 | ```elixir 81 | iex> for i <- [:a, :b, :c], j <- [1, 2], do: {i, j} 82 | [a: 1, a: 2, b: 1, b: 2, c: 1, c: 2] 83 | ``` 84 | 85 | 关于多生成器、过滤器的更高级些的例子:计算毕达哥拉斯三元数(Pythagorean triples)。 86 | 毕氏三元数一组正整数满足`a * a + b * b = c * c`,让我们在文件`triples.exs`里写这个速构: 87 | 88 | ```elixir 89 | defmodule Triple do 90 | def pythagorean(n) when n > 0 do 91 | for a <- 1..n, 92 | b <- 1..n, 93 | c <- 1..n, 94 | a + b + c <= n, 95 | a*a + b*b == c*c, 96 | do: {a, b, c} 97 | end 98 | end 99 | ``` 100 | 101 | 然后,在终端里: 102 | 103 | ``` 104 | iex triple.exs 105 | ``` 106 | 107 | ```elixir 108 | iex> Triple.pythagorean(5) 109 | [] 110 | iex> Triple.pythagorean(12) 111 | [{3, 4, 5}, {4, 3, 5}] 112 | iex> Triple.pythagorean(48) 113 | [{3, 4, 5}, {4, 3, 5}, {5, 12, 13}, {6, 8, 10}, {8, 6, 10}, {8, 15, 17}, 114 | {9, 12, 15}, {12, 5, 13}, {12, 9, 15}, {12, 16, 20}, {15, 8, 17}, {16, 12, 20}] 115 | ``` 116 | 117 | Finally, keep in mind that variable assignments inside the comprehension, be it in generators, filters or inside the block, are not reflected outside of the comprehension. 118 | 119 | 需要记住的是,在生成器、过滤器或者代码块中赋值的变量,不会暴露到速构外面去。 120 | 121 | ## 比特串生成器 122 | 123 | 速构也支持比特串作为生成器,这种生成器在处理比特流时非常有用。 124 | 下面的例子中,程序接收一个表示像素颜色的比特串(格式为<<像素1的R值,像素1的G值,像素1的B值, 125 | 像素2的R值,像素2的G...>>),把它转换为三元元组的列表: 126 | 127 | ```elixir 128 | iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>> 129 | iex> for <>, do: {r, g, b} 130 | [{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}] 131 | ``` 132 | 133 | 比特串生成器可以和“普通的”枚举类型生成器混合使用,过滤器也是。 134 | 135 | ## `:into`选项 136 | 137 | 在上面的例子中,速构返回列表作为结果。 138 | 但是,通过使用```:into```选项,速构的结果可以插入到不同的数据结构中。 139 | 例如,你可以使用比特串生成器加上```:into```来轻松地移除字符串中的空格: 140 | 141 | ```elixir 142 | iex> for <>, c != ?\s, into: "", do: <> 143 | "helloworld" 144 | ``` 145 | 146 | 集合、图、其他字典类型都可以传递给`:into`选项。总的来说,`:into`接受任何实现了_Collectable_协议的数据结构。 147 | 148 | `:into`选项一个常见的作用是,不用操作键,而改变图中元素的值: 149 | 150 | ```elixir 151 | iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val} 152 | %{"a" => 1, "b" => 4} 153 | ``` 154 | 155 | 再来一个使用流的例子。因为`IO`模块提供了流(既是Enumerable也是Collectable)。 156 | 你可以使用速构实现一个回声终端,让其返回任何输入的字母的大写形式: 157 | 158 | ```elixir 159 | iex> stream = IO.stream(:stdio, :line) 160 | iex> for line <- stream, into: stream do 161 | ...> String.upcase(line) <> "\n" 162 | ...> end 163 | ``` 164 | 165 | 现在在终端中输入任意字符串,你会看到同样的内容以大写形式被打印出来。 166 | 不幸的是,这个例子会让你的shell陷入到该速构代码中,只能用Ctrl+C两次来退出:-)。 167 | -------------------------------------------------------------------------------- /19-sigils.md: -------------------------------------------------------------------------------- 1 | 19-魔法印(Sigils) 2 | ========== 3 | 4 | >看看标题,这个“魔法印”是什么奇葩翻译? 5 | Sigil原意是“魔符,图章,印记”, 6 | 如古代西方魔幻传说中的巫女、魔法师画的封印或者召唤魔鬼的六芒星, 7 | 中国道士画的咒符,火影里面召唤守护兽的血印等。 8 | 在计算机编程领域,Sigil指的是在变量名称上做的标记,用来标明它的作用域或者类型什么的。 9 | 例如某语言里面`$var`中的`$`就是这样的东西,表示其为全局变量。 10 | 不同的魔法印赋予变量不同的属性,这么看,翻译成“魔法印”还挺带感呢。 11 | 本章(或者本文)所有的*sigil*都会翻译成“魔法印”或是采用英文原文。 12 | 13 | 我们已经学习了Elixir提供的字符串(双引号包裹)和字符列表(单引号包裹)。 14 | 但是对于Elixir中所有的*文本描述型数据类型*来说,这些只是冰山一角。其它的, 15 | 例如*原子*也是一种文本描述型数据类型。 16 | 17 | Elixir的一个特点就是高可扩展性:开发者能够为特定的领域来扩展语言。 18 | 计算机科学的领域已是如此广阔。几乎无法设计一门语言来涵盖所有范围。 19 | 我们的打算是,与其创造出一种万能的语言,不如创造一种可扩展的语言, 20 | 让开发者可以根据所从事的领域来对语言进行扩展。 21 | 22 | 本章将讲述“魔法印(sigils)”,它是Elixir提供的处理文本描述型数据的一种机制。 23 | 24 | ## 19.1-正则表达式 25 | 魔法印以波浪号(~)起头,后面跟着一个字母,然后是分隔符。最常用的魔法印是~r, 26 | 代表[正则表达式](https://en.wikipedia.org/wiki/Regular_Expressions): 27 | 28 | ```elixir 29 | # A regular expression that returns true if the text has foo or bar 30 | iex> regex = ~r/foo|bar/ 31 | ~r/foo|bar/ 32 | iex> "foo" =~ regex 33 | true 34 | iex> "bat" =~ regex 35 | false 36 | ``` 37 | 38 | Elixir提供了Perl兼容的正则表达式(regex),由[PCRE库](http://www.pcre.org/)实现。 39 | 40 | 正则表达式支持修饰符(modifiers),例如```i```修饰符使该正则表达式无视大小写: 41 | 42 | ```elixir 43 | iex> "HELLO" =~ ~r/hello/ 44 | false 45 | iex> "HELLO" =~ ~r/hello/i 46 | true 47 | ``` 48 | 49 | 阅读[Regex模块](http://elixir-lang.org/docs/stable/elixir/Regex.html)获取关于其它修饰符的及其所支持的操作的更多信息。 50 | 51 | 目前为止,所有的例子都用了```/```界定正则表达式。事实上魔法印支持8种不同的分隔符: 52 | 53 | ```elixir 54 | ~r/hello/ 55 | ~r|hello| 56 | ~r"hello" 57 | ~r'hello' 58 | ~r(hello) 59 | ~r[hello] 60 | ~r{hello} 61 | ~r 62 | ``` 63 | 64 | 支持多种分隔符是因为在处理不同的魔法印的时候更加方便。 65 | 比如,使用括号作为正则表达式的分隔符会让人困惑,分不清括号是正则模式的一部分还是别的什么。 66 | 但是,括号对某些魔法印来说就很方便。 67 | 68 | ## 19.2-字符串、字符列表和单词魔法印 69 | 除了正则表达式,Elixir还提供了三种魔法印。 70 | 71 | 魔法印```~s``` 用来生成字符串,类似双引号的作用: 72 | 73 | ```elixir 74 | iex> ~s(this is a string with "quotes") 75 | "this is a string with \"quotes\"" 76 | ``` 77 | 通过这个例子可以看出,如果文本中有双引号,又不想逐个转义,可以用这种魔法印来包裹字符串。 78 | 79 | 魔法印```~c``` 用来生成字符列表: 80 | 81 | ```elixir 82 | iex> ~c(this is a string with "quotes") 83 | 'this is a string with "quotes"' 84 | ``` 85 | 86 | 魔法印```~w``` 用来生成单词,以空格分隔开: 87 | 88 | ```elixir 89 | iex> ~w(foo bar bat) 90 | ["foo", "bar", "bat"] 91 | ``` 92 | 93 | 魔法印```~w``` 还接受```c```,```s```和```a```修饰符(分别代表字符列表,字符串和原子) 94 | 来选择结果的类型: 95 | 96 | ```elixir 97 | iex> ~w(foo bar bat)a 98 | [:foo, :bar, :bat] 99 | ``` 100 | 101 | 除了小写的魔法印,Elixir还支持大写的魔法印。如,```~s```和```~S```都返回字符串, 102 | 前者会解释转义字符而后者不会: 103 | 104 | ```elixir 105 | iex> ~s(String with escape codes \x26 interpolation) 106 | "String with escape codes & interpolation" 107 | iex> ~S(String without escape codes and without #{interpolation}) 108 | "String without escape codes and without \#{interpolation}" 109 | ``` 110 | 111 | 字符串和字符列表支持以下转义字符: 112 | - \" 表示一个双引号 113 | - \' 表示一个单引号 114 | - \\\ 表示一个反斜杠 115 | - \a 响铃 116 | - \b 退格 117 | - \d 删除 118 | - \e 退出 119 | - \f 换页 120 | - \n 新行 121 | - \r 换行 122 | - \s 空格 123 | - \t 水平制表符 124 | - \v 垂直制表符 125 | - \DDD, \DD, \D 八进制数字(如\377) 126 | - \xDD 十六进制数字(如\x13) 127 | - \x{D...} 多个十六进制字符的十六进制数(如\x{abc13} 128 | 129 | 130 | 魔法印还支持多行文本(heredocs),使用的是三个双引号或单引号: 131 | 132 | ```elixir 133 | iex> ~s""" 134 | ...> this is 135 | ...> a heredoc string 136 | ...> """ 137 | ``` 138 | 139 | 最常见的有多行文本的魔法印就是写注释文档了。 140 | 例如,如果你要在注释里写一些转义字符,这有可能会报错。 141 | 142 | ```elixir 143 | @doc """ 144 | Converts double-quotes to single-quotes. 145 | 146 | ## Examples 147 | 148 | iex> convert("\\\"foo\\\"") 149 | "'foo'" 150 | 151 | """ 152 | def convert(...) 153 | ``` 154 | 155 | 使用```~S```,我们就可以避免问题: 156 | 157 | ```elixir 158 | @doc ~S""" 159 | Converts double-quotes to single-quotes. 160 | 161 | ## Examples 162 | 163 | iex> convert("\"foo\"") 164 | "'foo'" 165 | 166 | """ 167 | def convert(...) 168 | ``` 169 | 170 | ### 19.3-自定义魔法印 171 | 本章开头提到过,魔法印是可扩展的。事实上,魔法印```~r/foo/i```等于是 172 | 用两个参数调用了函数```sigil_r```: 173 | 174 | ```elixir 175 | iex> sigil_r(<<"foo">>, 'i') 176 | ~r"foo"i 177 | ``` 178 | 179 | 就是说,我们可以通过该函数阅读魔法印```~r```的文档: 180 | 181 | ```elixir 182 | iex> h sigil_r 183 | ... 184 | ``` 185 | 186 | 我们也可以通过实现相应的函数来提供我们自己的魔法印。例如,我们来实现一个```~i(N)```魔法印, 187 | 返回整数: 188 | 189 | ```elixir 190 | iex> defmodule MySigils do 191 | ...> def sigil_i(string, []), do: String.to_integer(string) 192 | ...> end 193 | iex> import MySigils 194 | iex> ~i("13") 195 | 13 196 | ``` 197 | 198 | 魔法印通过宏,可以用来做一些发生在*编译时*的工作。例如,正则表达式在编译时会被编译, 199 | 而在执行的时候就不必再被编译了。 200 | 如果你对此主题感兴趣,可以多阅读关于宏的资料,并且阅读Kernel模块中那些魔法印的实现。 201 | -------------------------------------------------------------------------------- /2-basic-types.md: -------------------------------------------------------------------------------- 1 | 2-基本类型 2 | ========== 3 | 4 | 本章介绍Elixir的基本类型。Elixir主要的基本类型有: 5 | 整型(integer),浮点(float),布尔(boolean),原子(atom,又称symbol符号), 6 | 字符串(string),列表(list)和元组(tuple)等。 7 | 8 | 它们在iex中显示如下: 9 | ```elixir 10 | iex> 1 # integer 11 | iex> 0x1F # integer 12 | iex> 1.0 # float 13 | iex> true # boolean 14 | iex> :atom # atom / symbol 15 | iex> "elixir" # string 16 | iex> [1, 2, 3] # list 17 | iex> {1, 2, 3} # tuple 18 | ``` 19 | 20 | ## 2.1-基本算数运算 21 | 打开```iex```,输入以下表达式: 22 | ```elixir 23 | iex> 1 + 2 24 | 3 25 | iex> 5 * 5 26 | 25 27 | iex> 10 / 2 28 | 5.0 29 | ``` 30 | 31 | >```10 / 2```返回了一个浮点型的5.0而非整型的5,这是预期的。 32 | 在Elixir中,```/```运算符总是返回浮点型数值。 33 | 34 | 如果你想进行整型除法,或者求余数,可以使用函数```div```和```rem```。 35 | (rem的意思是division remainder,余数): 36 | ```elixir 37 | iex> div(10, 2) 38 | 5 39 | iex> div 10, 2 40 | 5 41 | iex> rem 10, 3 42 | 1 43 | ``` 44 | >在写函数参数时,括号是可选的。(ruby程序员会心一笑) 45 | 46 | Elixir支持用 **捷径(shortcut)** 书写二进制、八进制、十六进制整数。如: 47 | ```elixir 48 | iex> 0b1010 49 | 10 50 | iex> 0o777 51 | 511 52 | iex> 0x1F 53 | 31 54 | ``` 55 | >揉揉眼,八进制是```0o```,数字0 + 小写o。 56 | 57 | 输入浮点型数字需要一个小数点,且在其后至少有一位数字。 58 | Elixir支持使用```e```来表示指数: 59 | ```elixir 60 | iex> 1.0 61 | 1.0 62 | iex> 1.0e-10 63 | 1.0e-10 64 | ``` 65 | Elixir中浮点型都是64位双精度。 66 | 67 | ## 2.2-布尔 68 | Elixir使用```true```和```false```两个布尔值。 69 | ```elixir 70 | iex> true 71 | true 72 | iex> true == false 73 | false 74 | ``` 75 | Elixir提供了许多用以判断类型的函数,如```is_boolean/1```函数可以用来检查参数是不是布尔型。 76 | 77 | >在Elixir中,函数通过名称和参数个数(又称元数,arity)来识别。 78 | 如```is_boolean/1```表示名为```is_boolean```,接受一个参数的函数; 79 | 而```is_boolean/2```表示与其同名、但接受2个参数的**不同**函数。(只是打个比方,这样的is_boolean实际上不存在) 80 | 另外,```<函数名>/<元数>```这样的表述是为了在讲述函数时方便,在实际程序中如果调用函数, 81 | 是不用注明```/1```或```/2```的。 82 | 83 | ```elixir 84 | iex> is_boolean(true) 85 | true 86 | iex> is_boolean(1) 87 | false 88 | ``` 89 | 90 | 类似的函数还有```is_integer/1```,```is_float/1```,```is_number/1```, 91 | 分别测试参数是否是整型、浮点型或者两者其一。 92 | 93 | >可以在交互式命令行中使用```h```命令来打印函数或运算符的帮助信息。 94 | 如```h is_boolean/1```或```h ==/2```。 95 | 注意此处提及某个函数时,不但要给出名称,还要加上元数```/```。 96 | 97 | ## 2.3-原子 98 | 原子(atom)是一种常量,名字就是它的值。 99 | 有些语言中称其为 **符号(symbol)**(如ruby): 100 | ```elixir 101 | iex> :hello 102 | :hello 103 | iex> :hello == :world 104 | false 105 | ``` 106 | 107 | 布尔值```true```和```false```实际上就是原子: 108 | ```elixir 109 | iex> true == :true 110 | true 111 | iex> is_atom(false) 112 | true 113 | ``` 114 | 115 | ## 2.4-字符串 116 | 在Elixir中,字符串以 **双括号** 包裹,采用UTF-8编码: 117 | ```elixir 118 | iex> "hellö" 119 | "hellö" 120 | ``` 121 | 122 | Elixir支持字符串插值(和ruby一样使用```#{ ... }```): 123 | ```elixir 124 | iex> "hellö #{:world}" 125 | "hellö world" 126 | ``` 127 | 128 | 字符串可以直接包含换行符,或者其转义字符: 129 | ```elixir 130 | iex> "hello 131 | ...> world" 132 | "hello\nworld" 133 | iex> "hello\nworld" 134 | "hello\nworld" 135 | ``` 136 | 137 | 你可以使用```IO```模块(module)里的```IO.puts/1```方法打印字符串: 138 | ```elixir 139 | iex> IO.puts "hello\nworld" 140 | hello 141 | world 142 | :ok 143 | ``` 144 | 函数```IO.puts/1```打印完字符串后,返回原子值```:ok```。 145 | 146 | 字符串在Elixir内部被表示为二进制数值(binaries),也就是一连串的字节(bytes): 147 | ```elixir 148 | iex> is_binary("hellö") 149 | true 150 | ``` 151 | >注意,二进制数值(binary)是Elixir内部的存储结构之一。 152 | 字符串、列表等类型在语言内部就表示为二进制数值,因此它们也可以被专门操作二进制数值的函数修改。 153 | 154 | 你可以查看字符串包含的字节数量: 155 | ```elixir 156 | iex> byte_size("hellö") 157 | 6 158 | ``` 159 | >为啥是6?不是5个字符么?注意里面有一个非ASCII字符```ö```,在UTF-8下被编码为2个字节。 160 | 161 | 我们可以使用专门的函数来返回字符串中的字符数量: 162 | ```elixir 163 | iex> String.length("hellö") 164 | 5 165 | ``` 166 | 167 | [String模块](http://elixir-lang.org/docs/stable/elixir/String.html)中提供了 168 | 很多符合Unicode标准的函数来操作字符串。如: 169 | ```elixir 170 | iex> String.upcase("hellö") 171 | "HELLÖ" 172 | ``` 173 | 174 | 记住,单引号和双引号包裹的字符串在Elixir中是两种不同的数据类型: 175 | ```elixir 176 | iex> 'hellö' == "hellö" 177 | false 178 | ``` 179 | 我们将在之后关于“二进制、字符串与字符列表”章节中详细讲述它们的区别。 180 | 181 | ## 2.5-匿名函数 182 | 在Elixir中,使用关键字```fn```和```end```来界定函数。如: 183 | ```elixir 184 | iex> add = fn a, b -> a + b end 185 | #Function<12.71889879/2 in :erl_eval.expr/5> 186 | iex> is_function(add) 187 | true 188 | iex> is_function(add, 2) 189 | true 190 | iex> is_function(add, 1) 191 | false 192 | iex> add.(1, 2) 193 | 3 194 | ``` 195 | 在Elixir中,函数是 **一等公民**。你可以将函数作为参数传递给其他函数,就像整型和浮点型一样。 196 | 在上面的例子中,我们向函数```is_function/1```传递了由变量```add```表示的匿名函数, 197 | 结果返回```true```。 198 | 我们还可以调用函数```is_function/2```来判断该参数函数的元数(参数个数)。 199 | 200 | 注意,在调用一个匿名函数时,在变量名和写参数的括号之间要有个 **点号(.)**。 201 | 202 | 匿名函数是闭包,意味着它们可以保留其定义的作用域(scope)内的其它变量值: 203 | ```elixir 204 | iex> add_two = fn a -> add.(a, 2) end 205 | #Function<6.71889879/1 in :erl_eval.expr/5> 206 | iex> add_two.(2) 207 | 4 208 | ``` 209 | 这个例子定义的匿名函数```add_two```它内部使用了之前在同一个iex内定义好的```add```变量。 210 | 但要注意,在匿名函数内修改了所引用的外部变量的值,并不实际反映到该变量上: 211 | ```elixir 212 | iex> x = 42 213 | 42 214 | iex> (fn -> x = 0 end).() 215 | 0 216 | iex> x 217 | 42 218 | ``` 219 | 这个例子中匿名函数把引用了外部变量x,并修改它的值为0。这时函数执行后,外部的x没有被影响。 220 | 221 | ## 2.6-(链式)列表 222 | Elixir使用方括号标识列表。列表可以包含任意类型的值: 223 | ```elixir 224 | iex> [1, 2, true, 3] 225 | [1, 2, true, 3] 226 | iex> length [1, 2, 3] 227 | 3 228 | ``` 229 | 230 | 两个列表可以使用```++/2```拼接,使用```--/2```做“减法”: 231 | ```elixir 232 | iex> [1, 2, 3] ++ [4, 5, 6] 233 | [1, 2, 3, 4, 5, 6] 234 | iex> [1, true, 2, false, 3, true] -- [true, false] 235 | [1, 2, 3, true] 236 | ``` 237 | 238 | 本教程将多次涉及列表头(head)和尾(tail)的概念。 239 | 列表的头指的是第一个元素,而尾指的是除了第一个元素以外,其它元素组成的列表。 240 | 它们分别可以用函数```hd/1```和```tl/1```从原列表中取出: 241 | ```elixir 242 | iex> list = [1,2,3] 243 | iex> hd(list) 244 | 1 245 | iex> tl(list) 246 | [2, 3] 247 | ``` 248 | 249 | 尝试从一个空列表中取出头或尾将会报错: 250 | ```elixir 251 | iex> hd [] 252 | ** (ArgumentError) argument error 253 | ``` 254 | 255 | ## 2.7-元组 256 | Elixir使用大括号(花括号)定义元组(tuples)。类似列表,元组也可以承载任意类型的数据: 257 | ```elixir 258 | iex> {:ok, "hello"} 259 | {:ok, "hello"} 260 | iex> tuple_size {:ok, "hello"} 261 | 2 262 | ``` 263 | 元组使用 ***连续的内存空间*** 存储数据。 264 | 这意味着可以很方便地使用索引访问元组数据,以及获取元组大小(索引从0开始): 265 | ```elixir 266 | iex> tuple = {:ok, "hello"} 267 | {:ok, "hello"} 268 | iex> elem(tuple, 1) 269 | "hello" 270 | iex> tuple_size(tuple) 271 | 2 272 | ``` 273 | 274 | 也可以很方便地使用函数```put_elem/3```设置某个位置的元素值: 275 | ```elixir 276 | iex> tuple = {:ok, "hello"} 277 | {:ok, "hello"} 278 | iex> put_elem(tuple, 1, "world") 279 | {:ok, "world"} 280 | iex> tuple 281 | {:ok, "hello"} 282 | ``` 283 | 284 | 注意函数```put_elem/3```返回一个新元组。原来那个由变量tuple标识的元组没有被改变。 285 | 这是因为Elixir的数据类型是 **不可变的**。 286 | 这种不可变性使你永远不用担心你的数据会在某处被某些代码改变。 287 | 在处理并发程序时,这种不可变性有利于减少多个程序实体同时修改一个数据结构时引起的竞争以及其他麻烦。 288 | 289 | ## 2.8-列表还是元组? 290 | 列表与元组的区别:列表在内存中是以链表的形式存储的,一个元素指向下一个元素, 291 | 然后再下一个...直到到达列表末尾。 292 | 我们称这样的一对数据(元素值 和 指向下一个元素的指针)为列表的一个单元(cons cell)。 293 | 294 | 用Elixir语法表示这种模式: 295 | ```elixir 296 | iex> list = [1|[2|[3|[]]]] 297 | [1, 2, 3] 298 | ``` 299 | >列表方括号中的竖线(|)表示列表头与尾的分界。 300 | 301 | 这个原理意味着获取列表的长度是一个线性操作:我们必须遍历完整个列表才能知道它的长度。 302 | 但是列表的前置拼接操作很快捷: 303 | ```elixir 304 | iex> [0] ++ list 305 | [0, 1, 2, 3] 306 | iex> list ++ [4] 307 | [1, 2, 3, 4] 308 | ``` 309 | 310 | 上面例子中第一条语句是 __前置__ 拼接操作,执行起来很快。 311 | 因为它只是简单地添加了一个新列表单元,它的尾指针指向原先列表头部。而原先的列表没有任何变化。 312 | 313 | 第二条语句是 __后缀__ 拼接操作,执行速度较慢。 314 | 这是因为它 **重建** 了原先的列表,让原先列表的末尾元素指向那个新元素。 315 | 316 | 另一方面,元组在内存中是连续存储的。 317 | 这意味着获取元组大小,或者使用索引访问元组元素的操作十分快速。 318 | 但是元组在修改或添加元素时开销很大,因为这些操作会在内存中对元组的进行整体复制。 319 | 320 | 这些讨论告诉我们当如何在不同的情况下选择使用不同的数据结构。 321 | 322 | 函数常用元组来返回多个信息。如```File.read/1```,它读取文件内容,返回一个元组: 323 | ```elixir 324 | iex> File.read("path/to/existing/file") 325 | {:ok, "... contents ..."} 326 | iex> File.read("path/to/unknown/file") 327 | {:error, :enoent} 328 | ``` 329 | 330 | 如果传递给函数```File.read/1```的文件路径有效,那么函数返回一个元组, 331 | 其首元素是原子```:ok```,第二个元素是文件内容。 332 | 如果路径无效,函数也将返回一个元组,其首元素是原子```:error```,第二个元素是错误信息。 333 | 334 | 大多数情况下,Elixir会引导你做正确的事。 335 | 比如有个叫```elem/2```的函数,它使用索引来访问一个元组元素。 336 | 这个函数没有相应的列表版本,因为根据存储机制,列表不适用通过索引来访问: 337 | ```elixir 338 | iex> tuple = {:ok, "hello"} 339 | {:ok, "hello"} 340 | iex> elem(tuple, 1) 341 | "hello" 342 | ``` 343 | 344 | 当需要计算某数据结构包含的元素个数时,Elixir遵循一个简单的规则: 345 | 如果操作在常数时间内完成(答案是提前算好的),这样的函数通常被命名为 ```*size```。 346 | 而如果操作需要显式计数,那么该函数通常命名为 ```*length```。 347 | 348 | 例如,目前讲到过的4个计数函数:```byte_size/1```(用来计算字符串有多少字节),```tuple_size/1``` 349 | (用来计算元组大小),```length/1```(计算列表长度) 350 | 以及```String.length/1```(计算字符串中的字符数)。 351 | 352 | 按照命名规则,当我们用```byte_size```获取字符串所占字节数时,开销较小。 353 | 但是当我们用```String.length```获取字符串unicode字符个数时,需要遍历整个字符串,开销较大。 354 | 355 | 除了本章介绍的数据类型,Elixir还提供了 **Port**,**Reference** 和 **PID** 三个数据类型(它们常用于进程交互)。这些数据类型将在讲解进程时详细介绍。 356 | -------------------------------------------------------------------------------- /20-typespecs-behaviors.md: -------------------------------------------------------------------------------- 1 | 20-Typespecs和behaviors 2 | ======================= 3 | 4 | ## 类型(type)和规格说明(spec) 5 | 6 | Elixir是一门动态类型语言,Elixir中所有数据类型都是在运行时动态推定的。 7 | 然而,Elixir还提供了 **typespecs** 标记,用来: 8 | 9 | 1. 声明自定义数据类型 10 | 2. 声明含有显式类型说明的函数签名(即函数的规格说明) 11 | 12 | ### 函数的规格说明(spec) 13 | 14 | 默认地,Elixir提供了一些基础数据类型,表示为 `integer` 或者 `pid`。 15 | 还有一些复杂情形:如函数`round/1`为例,它对一个float类型的数值四舍五入。 16 | 它以一个`number`(一个`integer`或`float`)作为参数,返回一个`integer`。 17 | 18 | 那么,它在[round函数的文档](http://elixir-lang.org/docs/stable/elixir/Kernel.html#round/1) 19 | 里面记载的函数签名为: 20 | 21 | ``` 22 | round(number) :: integer 23 | ``` 24 | 25 | `::` 表示其左边的函数 *返回* 一个其右面声明的类型的值。函数名后面括号中是参数类型的列表。 26 | 27 | 如果想特别注明某个函数的参数类型及返回值类型,那么可以在定义函数的时候, 28 | 在函数前面使用`@spec`指令附加上函数的规格说明(spec)。 29 | 30 | 比如,在函数库源码中,函数`round/1`是这么写的: 31 | 32 | ```elixir 33 | @spec round(number) :: integer 34 | def round(number), do: # 具体实现 ... 35 | ``` 36 | 37 | Elixir还支持组合类型。例如,整数的列表,它的类型表示为`[integer]`。 38 | 可以阅读[typespecs的文档](http://elixir-lang.org/docs/stable/elixir/typespecs.html) 39 | 查看Elixir提供的所有内建类型的表示方法。 40 | 41 | ### 定义自定义类型 42 | 43 | Elixir提供了许多有用的内建类型,而且也方便创建自定义类型应用于特定场景。 44 | 方法是在定义的时候,加上`@type`指令。 45 | 46 | 比如我们有个模块叫做`LuosyCalculator`,可以执行常见的算术计算(如求和、计算乘积等)。 47 | 但是,它的函数不是返回结果数值,而是返回一个元組, 48 | 该元組第一个元素是计算结果,第二个是随机的文字记号。 49 | 50 | ```elixir 51 | defmodule LousyCalculator do 52 | @spec add(number, number) :: {number, String.t} 53 | def add(x, y), do: { x + y, "你用计算器算这个?!" } 54 | 55 | @spec multiply(number, number) :: {number, String.t} 56 | def multiply(x, y), do: { x * y, "老天,不是吧?!" } 57 | end 58 | ``` 59 | 60 | 从例子中可以看出,元组是复合类型。每个元组都定义了其具体元素类型。 61 | 至于为何是`String.t`而不是`string`的原因,可以参考 62 | [这篇文章](http://elixir-lang.org/docs/stable/elixir/typespecs.html#Notes), 63 | 此处不多说明。 64 | 65 | 像这样定义函数规格说明是没问题,但是一次次重复写这种复合类型的 66 | 表示方法`{number, String.t}`,很快会厌烦的吧。 67 | 我们可以使用`@type`指令来声明我们自定义的类型: 68 | 69 | ```elixir 70 | defmodule LousyCalculator do 71 | @typedoc """ 72 | Just a number followed by a string. 73 | """ 74 | @type number_with_remark :: {number, String.t} 75 | 76 | @spec add(number, number) :: number_with_remark 77 | def add(x, y), do: {x + y, "You need a calculator to do that?"} 78 | 79 | @spec multiply(number, number) :: number_with_remark 80 | def multiply(x, y), do: {x * y, "It is like addition on steroids."} 81 | end 82 | ``` 83 | 84 | 指令`@typedoc`,和`@doc`或`@moduledoc`指令类似,用来解释说明自定义的类型,放在`@type`前面。 85 | 86 | 另外,通过`@type`定义的自定义类型,实际上也是模块的成员,可以被外界访问: 87 | 88 | ```elixir 89 | defmodule QuietCalculator do 90 | @spec add(number, number) :: number 91 | def add(x, y), do: make_quiet(LousyCalculator.add(x, y)) 92 | 93 | @spec make_quiet(LousyCalculator.number_with_remark) :: number 94 | defp make_quiet({num, _remark}), do: num 95 | end 96 | ``` 97 | 98 | 如果想要将某个自定义类型保持私有,可以使用 `@typep` 指令代替 `@type` 。 99 | 100 | ### 静态代码分析 101 | 102 | 给函数等元素标记类型或者签名的作用,不仅仅是被用来作为程序文档说明。举个例子, 103 | Erlang工具[Dialyzer][Dialyzer](http://www.erlang.org/doc/man/dialyzer.html) 104 | 通过这些类型或者签名标记,进行代码静态分析。 105 | 这就是为什么我们在 `QuiteCalculator` 例子中, 106 | 即使 `make_quite/1` 是个私有函数,也写了函数规格说明。 107 | 108 | ## 行为(behavior) 109 | 110 | 许多模块公用相同的公共API。可以参考下[Plug](https://github.com/elixir-lang/plug), 111 | 正如它的描述所言,是一个用于互联网应用的、可编辑的模块的**规格声明**。 112 | 每个所谓*plug*就是一个**必须**实现至少两个公共函数:`init/1`和`call/2`的模块。 113 | 114 | 行为提供了一种方法,用来: 115 | 116 | * 定义一系列必须实现的函数 117 | * 确保模块实现所有这些函数 118 | 119 | 你也可以把这些行为想象为面向对象语言里的接口:模块必须实现的一系列函数签名。 120 | 121 | ### 定义行为(Defining behaviors) 122 | 123 | 假如说我们希望实现一系列parser,每个parser解析结构化的数据:比如,一个JSON parser或是YAML parser。 124 | 这两个parser的*行为*几近相同: 125 | 它们都提供一个`parse/1`函数和一个`extensions/0`函数。`parse/1`函数返回一个数据对应的Elixir表达。 126 | 而`extensions/0`函数返回一个可被其解析的文件的扩展名列表(如,JSON文件是`.json`)。 127 | 128 | 我们可以创建一个名为`Parser`的行为: 129 | 130 | ```elixir 131 | defmodule Parser do 132 | @callback parse(String.t) :: any 133 | @callback extensions() :: [String.t] 134 | end 135 | ``` 136 | 137 | 那么,采用`Parser`这个行为的模块,必须实现所有被`@callback`指令标记的函数。正如你所看到的, 138 | `@callback`指令不但可以接受一个函数名,还可以接受一个函数规格定义(我们在本文开头讲述的,函数的spec)。 139 | 140 | 141 | ### 采用行为(adopting behavior) 142 | 143 | 模块采用一个行为的语法非常直白: 144 | 145 | ```elixir 146 | defmodule JSONParser do 147 | @behaviour Parser 148 | 149 | def parse(str), do: # ... parse JSON 150 | def extensions, do: ["json"] 151 | end 152 | ``` 153 | 154 | ```elixir 155 | defmodule YAMLParser do 156 | @behaviour Parser 157 | 158 | def parse(str), do: # ... parse YAML 159 | def extensions, do: ["yml"] 160 | end 161 | ``` 162 | 163 | 如果一个模块采用了一个尚未完全实现其所需回调方法的**行为(behavior)**,这将生成一个编译时错误。 164 | -------------------------------------------------------------------------------- /21-erlang-lib.md: -------------------------------------------------------------------------------- 1 | 21-Erlang库 2 | ============ 3 | 4 | 正如前文所言,Elixir是基于Erlang实现的编程语言。 5 | 对于Erlang语言库,Elixir提供了完善的交互能力。 6 | 而且,实际上,Elixir不只是简单地对Erlang库功能进行语言上的包装(是的,没有Elixir版本的对应Erlang库函数), 7 | 而是直接连接Erlang代码(因为同源,Elixir以特定语法来直接调用Erlang库函数)。 8 | 本章将展示一些Elixir中没有,但是常用(常见+有用)的Erlang功能函数。 9 | 10 | >随着对Elixir更加深入的学习和使用,你可能会更多地阅读参考Erlang的 11 | [标准库手册](http://erlang.org/doc/apps/stdlib/index.html)。 12 | 13 | ## 二进制串模块 14 | 15 | 内建的Elixir字符串模块用来处理UTF-8编码过的二进制串(binaries)。 16 | 而Erlang的[二进制串模块](http://erlang.org/doc/man/binary.html)可能对你更加有用, 17 | 因为它可以处理的二进制串不一定非要是UTF-8编码的: 18 | 19 | ```iex 20 | iex> String.to_char_list "Ø" 21 | [216] 22 | iex> :binary.bin_to_list "Ø" 23 | [195, 152] 24 | ``` 25 | 26 | 从上面的例子你就能看出一些区别来了;`String`模块返回UTF-8的字符码, 27 | 而`:binary`是原始的数据字节。 28 | 29 | >Elixir引用Erlang的库函数十分简单。正如这个例子所示,在Erlang库模块名称前面加上冒号, 30 | 就可以直接调用Erlang的库函数了。 31 | 32 | ## 格式化的字符串输出 33 | 34 | Elixir中并没有类似于C中的`printf`函数。作为一个可选项,你可以使用字符串插值来完成同样的功能: 35 | 36 | ```iex 37 | iex> f = Float.to_string(:math.pi, decimals: 3) |> String.rjust(10) 38 | iex> str = "Pi is approximately given by: #{f}" 39 | "Pi is approximately given by: 3.142" 40 | ``` 41 | 42 | 另外,还可以用Erlang标准库中的`:io.format/2`和`:io_lib.format/2`函数。 43 | 第一个格式化后输出到终端,而第二个输出到一个iolist。具体格式化的语法和`pringf`略有区别, 44 | 详见[Erlang文档](http://erlang.org/doc/man/io.html#format-1): 45 | 46 | ```iex 47 | iex> :io.format("Pi is approximately given by:~10.3f~n", [:math.pi]) 48 | Pi is approximately given by: 3.142 49 | :ok 50 | iex> to_string :io_lib.format("Pi is approximately given by:~10.3f~n", [:math.pi]) 51 | "Pi is approximately given by: 3.142\n" 52 | ``` 53 | 54 | 另外需要注意的是Erlang的格式化函数中对Unicode的处理。 55 | 56 | ## 日历模块 57 | 58 | [日历模块](http://erlang.org/doc/man/calendar.html) 包含本地时间和标准时间的转换函数, 59 | 以及其它一些时间函数。 60 | 61 | ```iex 62 | iex> :calendar.day_of_the_week(1980, 6, 28) 63 | 6 64 | iex> {date, time} = :calendar.now_to_local_time(:erlang.timestamp) 65 | iex> date 66 | {2016, 2, 17} 67 | iex> time 68 | {22, 4, 55} 69 | ``` 70 | 71 | ## 加密模块 72 | 73 | [加密模块](http://erlang.org/doc/man/crypto.html) 包含哈希方法,数字签名, 74 | 加密等功能函数: 75 | 76 | ```iex 77 | iex> Base.encode16(:crypto.hash(:sha256, "Elixir")) 78 | "3315715A7A3AD57428298676C5AE465DADA38D951BDFAC9348A8A31E9C7401CB" 79 | ``` 80 | 81 | `:crypto` 模块不是Erlang的标准库,但是包含在了Erlang发行包中。 82 | 这要求你必须在项目的配置文件中列出`:crypto`模块作为依赖项。 83 | 通过修改`mix.exs`来加载该模块: 84 | 85 | ```elixir 86 | def application do 87 | [applications: [:crypto]] 88 | end 89 | ``` 90 | 91 | ## 有向图模块 92 | 93 | [有向图模块](http://erlang.org/doc/man/digraph.html) (以及 94 | [有向图模块工具](http://erlang.org/doc/man/digraph_utils.html)) 95 | 包含了处理有向图(有丁点和边构成)的函数。 96 | 创建了一个图实例之后,模块的算法即可帮助找寻顶点最短路径、图中的环等。 97 | 98 | 给出三个顶点,找寻从第一个到最后一个顶点的最短路径: 99 | 100 | ```iex 101 | iex> digraph = :digraph.new() 102 | iex> coords = [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}] 103 | iex> [v0, v1, v2] = (for c <- coords, do: :digraph.add_vertex(digraph, c)) 104 | iex> :digraph.add_edge(digraph, v0, v1) 105 | iex> :digraph.add_edge(digraph, v1, v2) 106 | iex> :digraph.get_short_path(digraph, v0, v2) 107 | [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}] 108 | ``` 109 | 110 | ## ETS(Erlang Term Storage):Erlang的“Term存储”机制 111 | 112 | 模块[`ets`](http://erlang.org/doc/man/ets.html)以及 113 | [`dets`](http://erlang.org/doc/man/dets.html) 114 | 分别处理在内存中、磁盘上存储大型数据结构。 115 | 116 | ETS创建一个表来存储元祖。默认情况下,ETS表是受保护的:只有owner进程才能写表, 117 | 其它进程只可以读。ETS提供了一些功能,可被当做简单的数据库、键值存储或cache机制使用。 118 | 119 | 作为副作用,`ets`模块中的函数会修改表的状态。 120 | 121 | ```iex 122 | iex> table = :ets.new(:ets_test, []) 123 | # Store as tuples with {name, population} 124 | iex> :ets.insert(table, {"China", 1_374_000_000}) 125 | iex> :ets.insert(table, {"India", 1_284_000_000}) 126 | iex> :ets.insert(table, {"USA", 322_000_000}) 127 | iex> :ets.i(table) 128 | <1 > {"USA", 322000000} 129 | <2 > {"China", 1_374_000_000} 130 | <3 > {"India", 1_284_000_000} 131 | ``` 132 | 133 | ## 数学模块 134 | 135 | [数学模块](http://erlang.org/doc/man/math.html) 包含了常用数学操作, 136 | 如三角函数、指数或底数函数等等。 137 | 138 | ```iex 139 | iex> angle_45_deg = :math.pi() * 45.0 / 180.0 140 | iex> :math.sin(angle_45_deg) 141 | 0.7071067811865475 142 | iex> :math.exp(55.0) 143 | 7.694785265142018e23 144 | iex> :math.log(7.694785265142018e23) 145 | 55.0 146 | ``` 147 | 148 | ## 队列(queue)模块 149 | 150 | [队列 `queue` 是一种数据结构](http://erlang.org/doc/man/queue.html) 151 | 实现了双向先进先出的高效率队列: 152 | 153 | ```iex 154 | iex> q = :queue.new 155 | iex> q = :queue.in("A", q) 156 | iex> q = :queue.in("B", q) 157 | iex> {value, q} = :queue.out(q) 158 | iex> value 159 | {:value, "A"} 160 | iex> {value, q} = :queue.out(q) 161 | iex> value 162 | {:value, "B"} 163 | iex> {value, q} = :queue.out(q) 164 | iex> value 165 | :empty 166 | ``` 167 | 168 | ## 随机值(rand)模块 169 | 170 | [`rand`模块函数](http://erlang.org/doc/man/rand.html) 可以返回随机值或是设置随机seed: 171 | 172 | ```iex 173 | iex> :rand.uniform() 174 | 0.8175669086010815 175 | iex> _ = :rand.seed(:exs1024, {123, 123534, 345345}) 176 | iex> :rand.uniform() 177 | 0.5820506340260994 178 | iex> :rand.uniform(6) 179 | 6 180 | ``` 181 | 182 | ## zip和zlib模块 183 | 184 | [`zip`模块](http://erlang.org/doc/man/zip.html) 可以让你读写磁盘或内存中的zip文件, 185 | 以及提取其文件信息等。 186 | 187 | 以下代码计算了zip压缩包中的文件数量: 188 | 189 | ```iex 190 | iex> :zip.foldl(fn _, _, _, acc -> acc + 1 end, 0, :binary.bin_to_list("file.zip")) 191 | {:ok, 633} 192 | ``` 193 | 194 | [`zlib`模块](http://erlang.org/doc/man/zlib.html) 处理zlib格式的数据压缩 195 | (如gzip中用的): 196 | 197 | ```iex 198 | iex> song = " 199 | ...> Mary had a little lamb, 200 | ...> His fleece was white as snow, 201 | ...> And everywhere that Mary went, 202 | ...> The lamb was sure to go." 203 | iex> compressed = :zlib.compress(song) 204 | iex> byte_size song 205 | 110 206 | iex> byte_size compressed 207 | 99 208 | iex> :zlib.uncompress(compressed) 209 | "\nMary had a little lamb,\nHis fleece was white as snow,\nAnd everywhere that Mary went,\nThe lamb was sure to go." 210 | ``` 211 | -------------------------------------------------------------------------------- /22-next.md: -------------------------------------------------------------------------------- 1 | 20-下一步 2 | ========== 3 | 4 | 还想学习更多?继续阅读吧! 5 | 6 | ## 构建第一个Elixir工程 7 | 8 | Elixir提供了一个构建工具叫做Mix。你可以简单地执行以下命令来开始构建项目: 9 | 10 | ``` 11 | mix new path/to/new/project 12 | ``` 13 | 14 | 这里已经写好了一份手册,讲述了如何构建一个Elixir应用程序,包括它自己的监督树,配置,测试等等。 15 | 这个应用程序是一个分布式的键-值对存储程序。它以键-值对的形式将数据存储在“桶”里, 16 | 并且把这些“桶”分发到不同的节点: 17 | 18 | - [Advanced Elixir](https://github.com/straightdave/advanced_elixir) 19 | 20 | ## 元编程 21 | 22 | 感谢语言上的元编程支持,Elixir具有可扩展性和高度定制性。 23 | Elixir的元编程主要是由“宏”实现。在许多情景,特别是开发DSL特别有用。 24 | 这里也有一份手册文档,讲述了宏背后的基础原理,以及如何利用宏实现元编程来创建DSL: 25 | 26 | - [Elixir元编程](https://github.com/straightdave/elixir_meta_programming) 27 | 28 | ## 社区和其它资源 29 | 30 | 社区内的[“学习”部分](http://elixir-lang.org/learning.html) 31 | 推荐了一些书籍、视频等资源来学习Elixir,探索其生态系统。 32 | 还有一些,如程序大会的演讲、开源项目等由社区创建的学习资料在那里。 33 | 34 | 记住如果遇到困难,可以访问 __#elixir-lang__ 频道(__irc.freenode.net__), 35 | 或是向邮件列表中发信。可以肯定那里会有人愿意提供帮助。收藏博客或是 36 | [订阅邮件列表](https://groups.google.com/group/elixir-lang-core) 37 | 以接收最新的新闻和声明。 38 | 39 | 别忘记还可以阅读Elixir的源码,其大部分使用Elixir写的(主要是lib那个目录下)。 40 | 或是[阅读Elixir的文档](http://elixir-lang.org/docs.html)。 41 | 42 | ## 来点Erlang 43 | 44 | Elixir运行于Erlang虚拟机。不久之后,Elixir的开发者会完成对所有Erlang库的连接。 45 | 以下这些在线资源包含了Erlang的基础知识及高级特性: 46 | 47 | - [Erlang语法速成](http://elixir-lang.org/crash-course.html) 48 | 提供了Erlang语法的简要介绍。每个代码片段都附有对等的Elixir代码。 49 | 这不但有助于你一窥Erlang的语法,还可以复习你在本指导书里学到的知识。 50 | 51 | - Erlang的官方站点有份简短的 52 | [图文教程](http://www.erlang.org/course/concurrent_programming.html) 53 | 简要描述了Erlang并行编程的原语。 54 | 55 | - [为你好学点Erlang吧](http://learnyousomeerlang.com/) 56 | 是极好的Erlang介绍:它的设计原则、标准库、最佳实践等等等等。 57 | 只要阅读上述速成,你就可以安全地忽略一些Erlang教课书前面介绍基础语法的几章。当阅读到 58 | [并发指南](http://learnyousomeerlang.com/the-hitchhikers-guide-to-concurrency) 59 | 时,真正的愉悦开始了。 60 | -------------------------------------------------------------------------------- /3-basic-ops.md: -------------------------------------------------------------------------------- 1 | 3-基本运算符 2 | ============ 3 | 4 | 通过前几章的学习,我们知道Elixir提供了 ```+,-,*,/``` 4个算术运算符,外加整数除法函数```div/2```和 5 | 取余函数```rem/2```。 6 | Elixir还提供了```++```和```--```运算符来操作列表: 7 | ```elixir 8 | iex> [1,2,3] ++ [4,5,6] 9 | [1,2,3,4,5,6] 10 | iex> [1,2,3] -- [2] 11 | [1,3] 12 | ``` 13 | 14 | 使用```<>```进行字符串拼接: 15 | ```elixir 16 | iex> "foo" <> "bar" 17 | "foobar" 18 | ``` 19 | 20 | Elixir还提供了三个布尔运算符:```or,and,not```。这三个运算符只接受布尔值作为 *第一个* 参数: 21 | ```elixir 22 | iex> true and true 23 | true 24 | iex> false or is_atom(:example) 25 | true 26 | ``` 27 | 28 | 如果提供了非布尔值作为第一个参数,会报异常: 29 | ```elixir 30 | iex> 1 and true 31 | ** (ArgumentError) argument error 32 | ``` 33 | 34 | 运算符```or```和```and```可短路,即它们仅在第一个参数无法决定整体结果的情况下才执行第二个参数: 35 | ```elixir 36 | iex> false and error("This error will never be raised") 37 | false 38 | 39 | iex> true or error("This error will never be raised") 40 | true 41 | ``` 42 | 43 | >如果你是Erlang程序员,Elixir中的```and```和```or```其实就是```andalso```和```orelse```运算符。 44 | 45 | 除了这几个布尔运算符,Elixir还提供```||```,```&&```和```!```运算符。它们可以接受任意类型的参数值。 46 | 在使用这些运算符时,除了 false 和 nil 的值都被视作 true: 47 | 48 | ```elixir 49 | # or 50 | iex> 1 || true 51 | 1 52 | iex> false || 11 53 | 11 54 | 55 | # and 56 | iex> nil && 13 57 | nil 58 | iex> true && 17 59 | 17 60 | 61 | # ! 62 | iex> !true 63 | false 64 | iex> !1 65 | false 66 | iex> !nil 67 | true 68 | ``` 69 | 70 | 根据经验,当参数确定是布尔时,使用```and```,```or```和```not```; 71 | 如果非布尔值(或不确定是不是),用```&&```,```||```和```!```。 72 | 73 | Elixir还提供了 ```==,!=,===,!==,<=,>=,<,>``` 这些比较运算符: 74 | 75 | ```elixir 76 | iex> 1 == 1 77 | true 78 | iex> 1 != 2 79 | true 80 | iex> 1 < 2 81 | true 82 | ``` 83 | 84 | 其中```==```和```===```的不同之处是后者在判断数字时更严格: 85 | 86 | ```elixir 87 | iex> 1 == 1.0 88 | true 89 | iex> 1 === 1.0 90 | false 91 | ``` 92 | 93 | 在Elixir中,可以判断不同类型数据的大小: 94 | ```elixir 95 | iex> 1 < :atom 96 | true 97 | ``` 98 | 99 | 这很实用。排序算法不必担心如何处理不同类型的数据。总体上,不同类型的排序顺序是: 100 | ``` 101 | number < atom < reference < functions < port < pid < tuple < maps < list < bitstring 102 | ``` 103 | 不用强记,只要知道有这么回事儿就可以。 104 | -------------------------------------------------------------------------------- /4-pattern-matching.md: -------------------------------------------------------------------------------- 1 | 4-模式匹配 2 | ========== 3 | 4 | 本章起教程进入 _不那么基础的_ 阶段,开始涉及函数式编程概念。 5 | 对之前没有函数式编程经验的人来说,这一章是一个基础,需要好好学习和理解。 6 | 7 | 在Elixir中,```=```运算符实际上叫做 *匹配运算符*。 8 | 本章将讲解如何使用```=```运算符来对各种数据结构进行模式匹配。 9 | 最后本章还会讲解pin运算符(```^```),用来访问某变量之前绑定的值。 10 | 11 | ## 4.1-匹配运算符 12 | 13 | 我们已经多次使用```=```符号进行变量的赋值操作: 14 | ```elixir 15 | iex> x = 1 16 | 1 17 | iex> x 18 | 1 19 | ``` 20 | 21 | 在Elixir中,```=```作为 *匹配运算符*。下面来学习这样的概念: 22 | ```elixir 23 | iex> 1 = x 24 | 1 25 | iex> 2 = x 26 | ** (MatchError) no match of right hand side value: 1 27 | ``` 28 | 29 | 注意```1 = x```是一个合法的表达式。 30 | 由于前面的例子给x赋值为1,因此在匹配时左右相同,所以它匹配成功了。而两侧不匹配的时候,MatchError将被抛出。 31 | 32 | 变量只有在匹配操作符```=```的左侧时才被赋值: 33 | ```elixir 34 | iex> 1 = unknown 35 | ** (RuntimeError) undefined function: unknown/0 36 | ``` 37 | 错误原因是unknown变量没有被赋过值,Elixir猜你想调用一个名叫```unknown/0```的函数, 38 | 但是找不到这样的函数。 39 | 40 | > 41 | 变量名在等号左边,Elixir认为是赋值表达式;变量名放在右边,Elixir认为是拿该变量的值和左边的值做匹配。 42 | 43 | ## 4.2-模式匹配 44 | 匹配运算符不光可以匹配简单数值,还能用来 *解构* 复杂的数据类型。例如,我们在元组上使用模式匹配: 45 | ```elixir 46 | iex> {a, b, c} = {:hello, "world", 42} 47 | {:hello, "world", 42} 48 | iex> a 49 | :hello 50 | iex> b 51 | "world" 52 | ``` 53 | 54 | 在两端不匹配的情况下,模式匹配会失败。比方说,匹配的两端的元组不一样长: 55 | ```elixir 56 | iex> {a, b, c} = {:hello, "world"} 57 | ** (MatchError) no match of right hand side value: {:hello, "world"} 58 | ``` 59 | 60 | 或者两端模式有区别(比如两端数据类型不同): 61 | ```elixir 62 | iex> {a, b, c} = [:hello, "world", "!"] 63 | ** (MatchError) no match of right hand side value: [:hello, "world", "!"] 64 | ``` 65 | 66 | 利用“匹配”的这个概念,我们可以匹配特定值;或者在匹配成功时,为某些变量赋值。 67 | 68 | 下面例子中先写好了匹配的左端,它要求右端必须是个元组,且第一个元素是原子```:ok```。 69 | ```elixir 70 | iex> {:ok, result} = {:ok, 13} 71 | {:ok, 13} 72 | iex> result 73 | 13 74 | 75 | iex> {:ok, result} = {:error, :oops} 76 | ** (MatchError) no match of right hand side value: {:error, :oops} 77 | ``` 78 | 79 | 用在列表上: 80 | ```elixir 81 | iex> [a, 2, 3] = [1, 2, 3] 82 | [1, 2, 3] 83 | iex> a 84 | 1 85 | ``` 86 | 87 | 列表支持匹配自己的```head```和```tail``` 88 | (这相当于同时调用```hd/1```和```tl/1```,给```head```和```tail```赋值): 89 | ```elixir 90 | iex> [head | tail] = [1, 2, 3] 91 | [1, 2, 3] 92 | iex> head 93 | 1 94 | iex> tail 95 | [2, 3] 96 | ``` 97 | 98 | 同```hd/1```和```tl/1```函数一样,以上代码不能对空列表使用: 99 | ```elixir 100 | iex> [h|t] = [] 101 | ** (MatchError) no match of right hand side value: [] 102 | ``` 103 | 104 | > 105 | [head|tail]这种形式不光在模式匹配时可以用,还可以用作向列表插入前置数值: 106 | ```elixir 107 | iex> list = [1, 2, 3] 108 | [1, 2, 3] 109 | iex> [0|list] 110 | [0, 1, 2, 3] 111 | ``` 112 | 113 | 模式匹配使得程序员可以容易地解构数据结构(如元组和列表)。 114 | 在后面我们还会看到,它是Elixir的一个基础,对其它数据结构同样适用,比如图和二进制。 115 | 116 | 小结: 117 | * 模式匹配使用```=```符号 118 | * 匹配中等号左右的“模式”必须相同 119 | * 变量在等号左侧才会被赋值 120 | * 变量在右侧时必须有值,Elixir拿这个值和左侧相应位置的元素做匹配 121 | 122 | 123 | ## 4.3-pin运算符 124 | 在Elixir中,变量可以被重新绑定: 125 | ```elixir 126 | iex> x = 1 127 | 1 128 | iex> x = 2 129 | 2 130 | ``` 131 | >Elixir可以给变量重新绑定(赋值)。 132 | 它带来一个问题,就是对一个单独变量(而且是放在左端)做匹配时, 133 | Elixir会认为这是一个重新绑定(赋值)操作,而不会当成匹配,执行匹配逻辑。 134 | 这里就要用到pin运算符。 135 | 136 | 如果你不想这样,可以使用pin运算符(^)。 137 | 加上了pin运算符的变量,在匹配时使用的值是本次匹配前就赋予的值: 138 | ```elixir 139 | iex> x = 1 140 | 1 141 | iex> ^x = 2 142 | ** (MatchError) no match of right hand side value: 2 143 | iex> {x, ^x} = {2, 1} 144 | {2, 1} 145 | iex> x 146 | 2 147 | ``` 148 | 149 | 注意如果一个变量在匹配中被引用超过一次,所有的引用都应该绑定同一个模式: 150 | ```elixir 151 | iex> {x, x} = {1, 1} 152 | 1 153 | iex> {x, x} = {1, 2} 154 | ** (MatchError) no match of right hand side value: {1, 2} 155 | ``` 156 | 157 | 有些时候,你并不在意模式匹配中的一些值。 158 | 可以把它们绑定到特殊的变量 “ _ ” (underscore)上。 159 | 例如,如果你只想要某列表的head,而不要tail值。你可以这么做: 160 | ```elixir 161 | iex> [h | _ ] = [1, 2, 3] 162 | [1, 2, 3] 163 | iex> h 164 | 1 165 | ``` 166 | 167 | 变量“ _ ”特殊之处在于它不能被读,尝试读取它会报“未绑定的变量”错误: 168 | ```elixir 169 | iex> _ 170 | ** (CompileError) iex:1: unbound variable _ 171 | ``` 172 | 173 | 尽管模式匹配看起来如此牛逼,但是语言还是对它的作用做了一些限制。 174 | 比如,你不能让函数调用作为模式匹配的左端。下面例子就是非法的: 175 | ```elixir 176 | iex> length([1,[2],3]) = 3 177 | ** (CompileError) iex:1: illegal pattern 178 | ``` 179 | 180 | 模式匹配介绍完了。 在以后的章节中,模式匹配是常用的语法结构。 181 | -------------------------------------------------------------------------------- /5-case-cond-if.md: -------------------------------------------------------------------------------- 1 | 5-流程控制 2 | ========== 3 | [case](#51-case) 4 | [卫兵子句中的表达式](#52-%E5%8D%AB%E5%85%B5%E5%AD%90%E5%8F%A5%E4%B8%AD%E7%9A%84%E8%A1%A8%E8%BE%BE%E5%BC%8F) 5 | [cond](#53-cond) 6 | [if和unless](#54-if%E5%92%8Cunless) 7 | [do语句块](#55-do%E8%AF%AD%E5%8F%A5%E5%9D%97) 8 | 9 | 本章讲解case,cond和if的流程控制结构。 10 | 11 | ## 5.1-case 12 | case将一个值与许多模式进行匹配,直到找到一个匹配成功的: 13 | ```elixir 14 | iex> case {1, 2, 3} do 15 | ...> {4, 5, 6} -> 16 | ...> "This clause won't match" 17 | ...> {1, x, 3} -> 18 | ...> "This clause will match and bind x to 2 in this clause" 19 | ...> _ -> 20 | ...> "This clause would match any value" 21 | ...> end 22 | ``` 23 | 24 | 如果与一个已赋值的变量做比较,要用pin运算符(^)标记该变量: 25 | ```elixir 26 | iex> x = 1 27 | 1 28 | iex> case 10 do 29 | ...> ^x -> "Won't match" 30 | ...> _ -> "Will match" 31 | ...> end 32 | ``` 33 | 34 | 可以加上卫兵子句(guard clauses)提供额外的条件: 35 | ```elixir 36 | iex> case {1, 2, 3} do 37 | ...> {1, x, 3} when x > 0 -> 38 | ...> "Will match" 39 | ...> _ -> 40 | ...> "Won't match" 41 | ...> end 42 | ``` 43 | 于是上面例子中,第一个待比较的模式多了一个条件:x必须是正数。 44 | 45 | ## 5.2-卫兵子句中的表达式 46 | Erlang中只允许以下表达式出现在卫兵子句中: 47 | - 比较运算符(==,!=,===,!==,>,<,<=,>=) 48 | - 布尔运算符(and,or)以及否定运算符(not,!) 49 | - 算数运算符(+,-,*,/) 50 | - <>和++如果左端是字面值 51 | - in运算符 52 | - 以下类型判断函数: 53 | - is_atom/1 54 | - is_binary/1 55 | - is_bitstring/1 56 | - is_boolean/1 57 | - is_float/1 58 | - is_function/1 59 | - is_function/2 60 | - is_integer/1 61 | - is_list/1 62 | - is_map/1 63 | - is_number/1 64 | - is_pid/1 65 | - is_reference/1 66 | - is_tuple/1 67 | - 外加以下函数: 68 | - abs(number) 69 | - bit_size(bitstring) 70 | - byte_size(bitstring) 71 | - div(integer, integer) 72 | - elem(tuple, n) 73 | - hd(list) 74 | - length(list) 75 | - map_size(map) 76 | - node() 77 | - node(pid | ref | port) 78 | - rem(integer, integer) 79 | - round(number) 80 | - self() 81 | - tl(list) 82 | - trunc(number) 83 | - tuple_size(tuple) 84 | 85 | 记住,卫兵子句中出现的错误不会漏出,只会简单地让卫兵条件失败: 86 | ```elixir 87 | iex> hd(1) 88 | ** (ArgumentError) argument error 89 | :erlang.hd(1) 90 | iex> case 1 do 91 | ...> x when hd(x) -> "Won't match" 92 | ...> x -> "Got: #{x}" 93 | ...> end 94 | "Got 1" 95 | ``` 96 | 97 | 如果case中没有一条模式能匹配,会报错: 98 | ```elixir 99 | iex> case :ok do 100 | ...> :error -> "Won't match" 101 | ...> end 102 | ** (CaseClauseError) no case clause matching: :ok 103 | ``` 104 | 105 | 匿名函数也可以像下面这样,用多个模式或卫兵条件来灵活地匹配该函数的参数: 106 | ```elixir 107 | iex> f = fn 108 | ...> x, y when x > 0 -> x + y 109 | ...> x, y -> x * y 110 | ...> end 111 | #Function<12.71889879/2 in :erl_eval.expr/5> 112 | iex> f.(1, 3) 113 | 4 114 | iex> f.(-1, 3) 115 | -3 116 | ``` 117 | 需要注意的是,所有case模式中表示的参数个数必须一致,否则会报错。 118 | 上面的例子两个待匹配模式都是x,y。如果再有一个模式表示的参数是x,y,z,那就不行: 119 | ```elixir 120 | iex(5)> f2 = fn 121 | ...(5)> x,y -> x+y 122 | ...(5)> x,y,z -> x+y+z 123 | ...(5)> end 124 | ** (CompileError) iex:5: cannot mix clauses with different arities in function definition 125 | (elixir) src/elixir_translator.erl:17: :elixir_translator.translate/2 126 | ``` 127 | 128 | ## 5.3-cond 129 | case是拿一个值去同多个值或模式进行匹配,匹配了就执行那个分支的语句。 130 | 然而,许多情况下我们要检查不同的条件,找到第一个结果为true的,执行它的分支。 131 | 这时我们用cond: 132 | ```elixir 133 | iex> cond do 134 | ...> 2 + 2 == 5 -> 135 | ...> "This will not be true" 136 | ...> 2 * 2 == 3 -> 137 | ...> "Nor this" 138 | ...> 1 + 1 == 2 -> 139 | ...> "But this will" 140 | ...> end 141 | "But this will" 142 | ``` 143 | 这样的写法和命令式语言里的```else if```差不多一个意思(尽管很少这么写)。 144 | 145 | 如果没有一个条件结果为true,会报错。因此,实际应用中通常会使用true作为最后一个条件。 146 | 因为即使上面的条件没有一个是true,那么该cond表达式至少还可以执行这最后一个分支: 147 | ```elixir 148 | iex> cond do 149 | ...> 2 + 2 == 5 -> 150 | ...> "This is never true" 151 | ...> 2 * 2 == 3 -> 152 | ...> "Nor this" 153 | ...> true -> 154 | ...> "This is always true (equivalent to else)" 155 | ...> end 156 | ``` 157 | 用法就好像许多语言中,switch语句中的default一样。 158 | 159 | 最后需要注意的是,cond视所有非false和nil的值为true: 160 | ```elixir 161 | iex> cond do 162 | ...> hd([1,2,3]) -> 163 | ...> "1 is considered as true" 164 | ...> end 165 | "1 is considered as true" 166 | ``` 167 | 168 | ## 5.4 if和unless 169 | 除了case和cond,Elixir还提供了两很常用的宏:```if/2```和```unless/2```, 170 | 用它们检查单个条件: 171 | ```elixir 172 | iex> if true do 173 | ...> "This works!" 174 | ...> end 175 | "This works!" 176 | iex> unless true do 177 | ...> "This will never be seen" 178 | ...> end 179 | nil 180 | ``` 181 | 182 | 如果给```if/2```的条件结果为false或者nil,那么它在do/end间的语句块就不会执行, 183 | 该表达式返回nil。```unless/2```相反。 184 | 185 | 它们都支持else语句块: 186 | ```elixir 187 | iex> if nil do 188 | ...> "This won't be seen" 189 | ...> else 190 | ...> "This will" 191 | ...> end 192 | "This will" 193 | ``` 194 | 195 | >有趣的是,```if/2```和```unless/2```是以宏的形式提供的,而不像在很多语言中那样是语句。 196 | 可以阅读文档或```if/2```的源码 197 | ([Kernel模块](http://elixir-lang.org/docs/stable/elixir/Kernel.html))。 198 | _Kernel_ 模块还定义了诸如```+/2```运算符和```is_function/2```函数。 199 | 它们默认被导入,因而在你的代码中可用。 200 | 201 | ## 5.5-```do```语句块 202 | 以上讲解的4种流程控制结构:case,cond,if和unless,它们都被包裹在do/end语句块中。 203 | 即使我们把if语句写成这样: 204 | ```elixir 205 | iex> if true, do: 1 + 2 206 | 3 207 | ``` 208 | 209 | 在Elixir中,do/end语句块方便地将一组表达式传递给```do:```。以下是等同的: 210 | ```elixir 211 | iex> if true do 212 | ...> a = 1 + 2 213 | ...> a + 10 214 | ...> end 215 | 13 216 | iex> if true, do: ( 217 | ...> a = 1 + 2 218 | ...> a + 10 219 | ...> ) 220 | 13 221 | ``` 222 | 我们称第二种语法使用了 **关键字列表(keyword lists)**。我们可以这样传递```else```: 223 | ```elixir 224 | iex> if false, do: :this, else: :that 225 | :that 226 | ``` 227 | 228 | 注意一点,do/end语句块永远是被绑定在最外层的函数调用上。例如: 229 | ```elixir 230 | iex> is_number if true do 231 | ...> 1 + 2 232 | ...> end 233 | ``` 234 | 将被解析为: 235 | ```elixir 236 | iex> is_number(if true) do 237 | ...> 1 + 2 238 | ...> end 239 | ``` 240 | 这使得Elixir认为你是要调用函数```is_number/2```(第一个参数是if true,第二个是语句块)。 241 | 这时就需要加上括号解决二义性: 242 | ```elixir 243 | iex> is_number(if true do 244 | ...> 1 + 2 245 | ...> end) 246 | true 247 | ``` 248 | 关键字列表在Elixir语言中占有重要地位,在许多函数和宏中都有使用。后文中还会对其进行详解。 249 | -------------------------------------------------------------------------------- /6-bin-str-charlist.md: -------------------------------------------------------------------------------- 1 | 6-二进制串、字符串和字符列表 2 | ======================== 3 | 4 | 在“基本类型”一章中,介绍了字符串,以及使用`is_binary/1`函数检查它: 5 | 6 | ```elixir 7 | iex> string = "hello" 8 | "hello" 9 | iex> is_binary string 10 | true 11 | ``` 12 | 13 | 本章将学习理解:二进制串(binaries)是个啥,它怎么和字符串(strings)扯上关系的; 14 | 以及用单引号包裹的值`'like this'`是啥意思。 15 | 16 | ## UTF-8和Unicode 17 | 18 | 字符串是UTF-8编码的二进制串。 19 | 为了弄清这句话的准确含义,我们要先理解两个概念:字节(bytes)和字符编码(code point)的区别。 20 | Unicode标准为我们已知的大部分字母分配了字符编码。 21 | 比如,字母`a`的字符编码是`97`,而字母`ł`的字符编码是`322`。 22 | 当把字符串`"hełło"`写到硬盘上的时候,需要将字符编码转化为字节。 23 | 如果我们遵循一个字节表示一个字符编码这个,那是写不了`"hełło"`的。 24 | 因为字母`ł`的编码是`322`,而一个字节所能存储的数值范围是`0`到`255`。 25 | 但是如你所见,确实能够在屏幕上显示`"hełło"`,说明还是有*某种*解决方法的,于是*编码*便出现了。 26 | 27 | 要用字节表示字符编码,我们需要用某种方式对其进行编码。 28 | Elixir选择UTF-8为主要并且默认的编码方式。 29 | 当我们说某个字符串是UTF-8编码的二进制串,指的是该字符串是一串字节, 30 | 这些字节以某种方式(即UTF-8编码)组织起来,表示特定的字符编码。 31 | 32 | 因为给字母`ł`分配的字符编码是`322`,因此在实际上需要一个以上的字节来表示。 33 | 这就是为什么我们会看到,调用函数`byte_size/1`和`String.length/1`的结果不一样: 34 | 35 | ```elixir 36 | iex> string = "hełło" 37 | "hełło" 38 | iex> byte_size string 39 | 7 40 | iex> String.length string 41 | 5 42 | ``` 43 | 44 | >注意:如果你使用Windows,你的终端有可能不是默认使用UTF-8编码方式。你需要在进入`iex(iex.bat)`之前, 45 | 首先执行`chcp 65001`命令来修改当前Session的编码方式。 46 | 47 | UTF-8需要1个字节来表示`h`,`e`,`o`的字符编码,用2个字节表示`ł`。 48 | 在Elixir中可以使用`?`运算符获取字符的编码: 49 | 50 | ```elixir 51 | iex> ?a 52 | 97 53 | iex> ?ł 54 | 322 55 | ``` 56 | 57 | 你还可以使用 58 | [String](http://elixir-lang.org/docs/stable/elixir/String.html)模块里的函数 59 | 将字符串切成单独的字符编码: 60 | 61 | ```elixir 62 | iex> String.codepoints("hełło") 63 | ["h", "e", "ł", "ł", "o"] 64 | ``` 65 | 66 | Elixir为字符串操作提供了强大的支持,它支持Unicode的许多操作。实际上,Elixir通过了文章 67 | [“字符串类型崩坏了”](http://mortoray.com/2013/11/27/the-string-type-is-broken/) 68 | 记录的所有测试。 69 | 70 | 然而,字符串只是故事的一小部分。如果字符串正如所言是二进制串,那我们使用`is_binaries/1`函数时, 71 | Elixir必须一个底层类型来支持字符串。事实亦如此,下面就来介绍这个底层类型---二进制串。 72 | 73 | ## 二进制串(以及比特串`bitstring`) 74 | 75 | 在Elixir中可以用`<<>>`定义一个二进制串: 76 | 77 | ```elixir 78 | iex> <<0, 1, 2, 3>> 79 | <<0, 1, 2, 3>> 80 | iex> byte_size(<<0, 1, 2, 3>>) 81 | 4 82 | ``` 83 | 84 | 一个二进制串只是一连串的字节而已。这些字节可以以任何方式组织,即使凑不成一个合法的字符串: 85 | 86 | ```elixir 87 | iex> String.valid?(<<239, 191, 191>>) 88 | false 89 | ``` 90 | 91 | 而字符串的拼接操作实际上就是二进制串的拼接操作: 92 | 93 | ```elixir 94 | iex> <<0, 1>> <> <<2, 3>> 95 | <<0, 1, 2, 3>> 96 | ``` 97 | 98 | 一个常见技巧是,通过给一个字符串尾部拼接一个空(null)字节`<<0>>`, 99 | 可以看到该字符串内部二进制串的样子: 100 | 101 | ```elixir 102 | iex> "hełło" <> <<0>> 103 | <<104, 101, 197, 130, 197, 130, 111, 0>> 104 | ``` 105 | 106 | 二进制串中的每个数值都表示一个字节,其数值最大范围是255。 107 | 二进制允许使用修改器显式标注一下那个数值的存储空间大小,使其可以存储超过255的数值; 108 | 或者将一个字符编码转换为utf8编码后的形式(变成多个字节的二进制串): 109 | 110 | ```elixir 111 | iex> <<255>> 112 | <<255>> 113 | iex> <<256>> # 被截断(truncated) 114 | <<0>> 115 | iex> <<256 :: size(16)>> # 使用16比特(bits)即2个字节来保存 116 | <<1, 0>> 117 | iex> <<256 :: utf8>> # 这个数字是一个字符的编码,将其使用utf8方式编码为字节 118 | "Ā" # 注意,在iex交互窗口中,所有可以作为合法字符串的二进制串,都会显示为字符串 119 | iex> <<256 :: utf8, 0>> # 尾部拼接个空字节,查看上一条命令结果内部实际的二进制串 120 | <<196, 128, 0>> 121 | ``` 122 | 123 | 如果一个字节是8个比特,那如果我们给一个大小是1比特的修改器会怎样?: 124 | 125 | ```elixir 126 | iex> <<1 :: size(1)>> 127 | <<1::size(1)>> 128 | iex> <<2 :: size(1)>> # 被截断(truncated) 129 | <<0::size(1)>> 130 | iex> is_binary(<< 1 :: size(1)>>) # 二进制串失格 131 | false 132 | iex> is_bitstring(<< 1 :: size(1)>>) 133 | true 134 | iex> bit_size(<< 1 :: size(1)>>) 135 | 1 136 | ``` 137 | 138 | 这样(每个元素长度是1比特)就不再是二进制串(人家每个元素是一个字节,起码8比特), 139 | 退化成为比特串(bitstring),意思就是一串比特! 140 | 所以,所以,二进制串就是一特殊的比特串,比特总数是8的倍数。 141 | 142 | 也可以对二进制串或比特串做模式匹配: 143 | 144 | ```elixir 145 | iex> <<0, 1, x>> = <<0, 1, 2>> 146 | <<0, 1, 2>> 147 | iex> x 148 | 2 149 | iex> <<0, 1, x>> = <<0, 1, 2, 3>> 150 | ** (MatchError) no match of right hand side value: <<0, 1, 2, 3>> 151 | ``` 152 | 153 | 注意,在没有修改器标识的情况下,二进制串中的每个元素都应该匹配8个比特长度。 154 | 因此上面最后的例子,匹配的左右两端不具有相同容量,因此出现错误。 155 | 156 | 下面是使用了修改器标识的匹配例子: 157 | 158 | ```elixir 159 | iex> <<0, 1, x :: binary>> = <<0, 1, 2, 3>> 160 | <<0, 1, 2, 3>> 161 | iex> x 162 | <<2, 3>> 163 | ``` 164 | 165 | 上面例子使用了`binary`修改器,指示`x`是个二进制串。(为啥不用单词的复数形式`binaries`搞不懂啊。) 166 | 这个修改器仅仅可以用在被匹配的串的*末尾元素*上。 167 | 168 | 跟上面例子同样的原理,使用字符串的连接操作符`<>`,效果相似: 169 | 170 | ```elixir 171 | iex> "he" <> rest = "hello" 172 | "hello" 173 | iex> rest 174 | "llo" 175 | ``` 176 | 177 | 关于二进制串/比特串的构造器`<< >>`完整的参考, 178 | 请见[Elixir的文档](http://elixir-lang.org/docs/stable/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1)。 179 | 180 | 总之,记住字符串是UTF-8编码后的二进制串,而二进制串是特殊的、元素数量是8的倍数的比特串。 181 | 尽管这种机制增加了Elixir在处理比特或字节时的灵活性, 182 | 而现实中99%的时候你只会用到`is_binary/1`和`byte_size/1`函数跟二进制串打交道。 183 | 184 | ## 字符列表(char lists) 185 | 186 | 字符列表就是字符的列表。 187 | 188 | ```elixir 189 | iex> 'hełło' 190 | [104, 101, 322, 322, 111] 191 | iex> is_list 'hełło' 192 | true 193 | iex> 'hello' 194 | 'hello' 195 | ``` 196 | 197 | 可以看出,比起包含字节,一个字符列表包含的是单引号所引用的一串字符各自的字符编码。 198 | 注意IEx遇到超出ASCII值范围的字符编码时,显示其字符编码的值,而不是字符。 199 | 双引号引用的是字符串(即二进制串),单引号表示的是字符列表(即,一个列表)。 200 | 201 | 实际应用中,字符列表常被用来做为同一些Erlang库交互的参数,因为这些老库不接受二进制串作为参数。 202 | 要将字符列表和字符串之间相互转换,可以使用函数`to_string/1`和`to_char_list/1`: 203 | 204 | ```elixir 205 | iex> to_char_list "hełło" 206 | [104, 101, 322, 322, 111] 207 | iex> to_string 'hełło' 208 | "hełło" 209 | iex> to_string :hello 210 | "hello" 211 | iex> to_string 1 212 | "1" 213 | ``` 214 | 215 | 注意这些函数是多态的。它们不但可以将字符列表转化为字符串,还能转化整数、原子等为字符串。 216 | -------------------------------------------------------------------------------- /7-keywords-map-dict.md: -------------------------------------------------------------------------------- 1 | 7-键值列表-图-字典 2 | ================ 3 | [键值列表](#71) 4 | [图](#72-%E5%9B%BEmaps) 5 | [字典](#73-%E5%AD%97%E5%85%B8dicts) 6 | 7 | 到目前还没有讲到任何关联性数据结构,即那种可以将一个或几个值关联到一个key上。 8 | 不同语言有不同的叫法,如字典,哈希,关联数组,图,等等。 9 | 10 | Elixir中有两种主要的关联性结构:键值列表(keyword list)和图(map)。 11 | 12 | ## 7.1-键值列表 13 | 在很多函数式语言中,常用二元元组的列表来表示关联性数据结构。在Elixir中也是这样。 14 | 当我们有了一个元组(不一定仅有两个元素的元组)的列表,并且每个元组的第一个元素是个 **原子**, 15 | 那就称之为键值列表: 16 | ```elixir 17 | iex> list = [{:a, 1}, {:b, 2}] 18 | [a: 1, b: 2] 19 | iex> list == [a: 1, b: 2] 20 | true 21 | iex> list[:a] 22 | 1 23 | ``` 24 | 25 | >当原子key和关联的值之间没有逗号分隔时,可以把原子的冒号拿到字母的后面。这时,原子与后面的数值之间要有一个空格。 26 | 27 | 如你所见,Elixir使用比较特殊的语法来定义这样的列表,但实际上它们会映射到一个元组列表。 28 | 因为它们是简单的列表而已,所有针对列表的操作,键值列表也可以用。 29 | 30 | 比如,可以用```++```运算符为列表添加元素: 31 | ```elixir 32 | iex> list ++ [c: 3] 33 | [a: 1, b: 2, c: 3] 34 | iex> [a: 0] ++ list 35 | [a: 0, a: 1, b: 2] 36 | ``` 37 | 上面例子中重复出现了```:a```这个key,这是允许的。 38 | 以这个key取值时,取回来的是第一个找到的(因为有序): 39 | ```elixir 40 | iex> new_list = [a: 0] ++ list 41 | [a: 0, a: 1, b: 2] 42 | iex> new_list[:a] 43 | 0 44 | ``` 45 | 46 | 键值列表十分重要,它有两大特点: 47 | - 有序 48 | - key可以重复(!仔细看上面两个示例) 49 | 50 | 例如,[Ecto库](https://github.com/elixir-lang/ecto)使用这两个特点 51 | 写出了精巧的DSL(用来写数据库query): 52 | ```elixir 53 | query = from w in Weather, 54 | where: w.prcp > 0, 55 | where: w.temp < 20, 56 | select: w 57 | ``` 58 | 59 | 这些特性使得键值列表成了Elixir中为函数提供额外选项的默认手段。 60 | 在第5章我们讨论了```if/2```宏,提到了下方的语法: 61 | ```elixir 62 | iex> if false, do: :this, else: :that 63 | :that 64 | ``` 65 | 66 | do: 和else: 就是键值列表!事实上代码等同于: 67 | ```elixir 68 | iex> if(false, [do: :this, else: :that]) 69 | :that 70 | ``` 71 | 72 | 当键值列表是函数最后一个参数时,方括号就成了可选的。 73 | 74 | 为了操作关键字列表,Elixir提供了 75 | [键值(keyword)模块](http://elixir-lang.org/docs/stable/elixir/Keyword.html)。 76 | 记住,键值列表就是简单的列表,和列表一样提供了线性的性能。 77 | 列表越长,获取长度或找到一个键值的速度越慢。 78 | 因此,关键字列表在Elixir中一般就作为函数调用的可选项。 79 | 如果你要存储大量数据,并且保证一个键只对应最多一个值,那就使用图。 80 | 81 | 对键值列表做模式匹配: 82 | ```elixir 83 | iex> [a: a] = [a: 1] 84 | [a: 1] 85 | iex> a 86 | 1 87 | iex> [a: a] = [a: 1, b: 2] 88 | ** (MatchError) no match of right hand side value: [a: 1, b: 2] 89 | iex> [b: b, a: a] = [a: 1, b: 2] 90 | ** (MatchError) no match of right hand side value: [a: 1, b: 2] 91 | ``` 92 | 尽管如此,对列表使用模式匹配很少用到。因为不但要元素个数相等,顺序还要匹配。 93 | 94 | ## 7.2-图(maps) 95 | 无论何时想用键-值结构,图都应该是你的第一选择。Elixir中,用```%{}```定义图: 96 | ```elixir 97 | iex> map = %{:a => 1, 2 => :b} 98 | %{2 => :b, :a => 1} 99 | iex> map[:a] 100 | 1 101 | iex> map[2] 102 | :b 103 | ``` 104 | 105 | 和键值列表对比,图有两主要区别: 106 | - 图允许任何类型值作为键 107 | - 图的键没有顺序 108 | 109 | 如果你向图添加一个已有的键,将会覆盖之前的键-值对: 110 | ```elixir 111 | iex> %{1 => 1, 1 => 2} 112 | %{1 => 2} 113 | ``` 114 | 115 | 如果图中的键都是原子,那么你也可以用键值列表中的一些语法: 116 | ```elixir 117 | iex> map = %{a: 1, b: 2} 118 | %{a: 1, b: 2} 119 | ``` 120 | 121 | 对比键值列表,图的模式匹配很是有用: 122 | ```elixir 123 | iex> %{} = %{:a => 1, 2 => :b} 124 | %{:a => 1, 2 => :b} 125 | iex> %{:a => a} = %{:a => 1, 2 => :b} 126 | %{:a => 1, 2 => :b} 127 | iex> a 128 | 1 129 | iex> %{:c => c} = %{:a => 1, 2 => :b} 130 | ** (MatchError) no match of right hand side value: %{2 => :b, :a => 1} 131 | ``` 132 | 如上所示,图A与另一个图B做匹配。 133 | 图B中只要包含有图A的键,那么两个图就能匹配上。若图A是个空的,那么任意图B都能匹配上。 134 | 但是如果图B里不包含图A的键,那就匹配失败了。 135 | 136 | 图还有个有趣的功能:它提供了特殊的语法来修改和访问原子键: 137 | ```elixir 138 | iex> map = %{:a => 1, 2 => :b} 139 | %{:a => 1, 2 => :b} 140 | iex> map.a 141 | 1 142 | iex> %{map | :a => 2} 143 | %{:a => 2, 2 => :b} 144 | iex> %{map | :c => 3} 145 | ** (ArgumentError) argument error 146 | ``` 147 | 148 | 使用上面两种语法要求的前提是所给的键是切实存在的。最后一条语句错误的原因就是键```:c```不存在。 149 | 150 | 未来几章中我们还将讨论结构体(structs)。结构体提供了编译时的保证,它是Elixir多态的基础。 151 | 结构体是基于图的,上面例子提到的修改键值的前提就变得十分重要。 152 | 153 | [图模块](http://elixir-lang.org/docs/stable/elixir/Map.html)提供了许多关于图的操作。 154 | 它提供了与键值列表许多相似的API,因为这两个数据结构都实现了字典的行为。 155 | 156 | >图是最近连同[EEP 43](http://www.erlang.org/eeps/eep-0043.html)被引入Erlang虚拟机的。 157 | Erlang 17提供了EEP的部分实现,只支持_一小部分_图功能。 158 | 这意味着图仅在存储不多的键时,图的性能还行。 159 | 为了解决这个问题,Elixir还提供了 160 | [HashDict模块](http://elixir-lang.org/docs/stable/elixir/HashDict.html)。 161 | 该模块提供了一个字典来支持大量的键,并且性能不错。 162 | 163 | ## 7.3-字典(Dicts) 164 | Elixir中,键值列表和图都被称作字典。 165 | 换句话说,一个字典就像一个接口(在Elixir中称之为行为behaviour)。 166 | 键值列表和图模块实现了该接口。 167 | 168 | 这个接口定义于[Dict模块](http://elixir-lang.org/docs/stable/elixir/Dict.html), 169 | 该模块还提供了底层实现的一个API: 170 | ```elixir 171 | iex> keyword = [] 172 | [] 173 | iex> map = %{} 174 | %{} 175 | iex> Dict.put(keyword, :a, 1) 176 | [a: 1] 177 | iex> Dict.put(map, :a, 1) 178 | %{a: 1} 179 | ``` 180 | 181 | 字典模块允许开发者实现自己的字典形式,提供一些特殊的功能。 182 | 字典模块还提供了所有字典类型都可以使用的函数。 183 | 如,```Dicr.equal?/2```可以比较两个字典类型(可以是不同的实现)。 184 | 185 | 你会疑惑些程序时用keyword,Map还是Dict模块呢?答案是:看情况。 186 | 187 | 如果你的代码期望接受一个关键字作为参数,那么使用简直列表模块。 188 | 如果你想操作一个图,那就使用图模块。 189 | 如果你想你的API对所有字典类型的实现都有用, 190 | 那就使用字典模块(确保以不同的实现作为参数测试一下)。 191 | -------------------------------------------------------------------------------- /8-modules.md: -------------------------------------------------------------------------------- 1 | 8-模块 2 | ====== 3 | 4 | [编译](#81-%E7%BC%96%E8%AF%91) 5 | [脚本模式](#82-%E8%84%9A%E6%9C%AC%E6%A8%A1%E5%BC%8F) 6 | [命名函数](#83-%E5%91%BD%E5%90%8D%E5%87%BD%E6%95%B0) 7 | [函数捕捉](#84-%E5%87%BD%E6%95%B0%E6%8D%95%E6%8D%89) 8 | [默认参数](#85-%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0) 9 | 10 | Elixir中我们把许多函数组织成一个模块。我们在前几章已经提到了许多模块, 11 | 如[String模块](http://elixir-lang.org/docs/stable/elixir/String.html): 12 | ```elixir 13 | iex> String.length "hello" 14 | 5 15 | ``` 16 | 17 | 创建自己的模块,用```defmodule```宏。用```def```宏在其中定义函数: 18 | ```elixir 19 | iex> defmodule Math do 20 | ...> def sum(a, b) do 21 | ...> a + b 22 | ...> end 23 | ...> end 24 | 25 | iex> Math.sum(1, 2) 26 | 3 27 | ``` 28 | >像ruby一样,模块名大写起头 29 | 30 | ## 8.1-编译 31 | 通常把模块写进文件,这样可以编译和重用。假如文件```math.ex```有如下内容: 32 | ```elixir 33 | defmodule Math do 34 | def sum(a, b) do 35 | a + b 36 | end 37 | end 38 | ``` 39 | 40 | 这个文件可以用```elixirc```进行编译: 41 | ```elixir 42 | $ elixirc math.ex 43 | ``` 44 | 45 | 这将生成名为```Elixir.Math.beam```的bytecode文件。 46 | 如果这时再启动iex,那么这个模块就已经可以用了(假如在含有该编译文件的目录启动iex): 47 | ```elixir 48 | iex> Math.sum(1, 2) 49 | 3 50 | ``` 51 | 52 | Elixir工程通常组织在三个文件夹里: 53 | - ebin,包括编译后的字节码 54 | - lib,包括Elixir代码(.ex文件) 55 | - test,测试代码(.exs文件) 56 | 57 | 实际项目中,构建工具Mix会负责编译,并且设置好正确的路径。 58 | 而为了学习方便,Elixir也提供了脚本模式,可以更灵活而不用编译。 59 | 60 | ## 8.2-脚本模式 61 | 除了.ex文件,Elixir还支持.exs脚本文件。 62 | Elixir对两种文件一视同仁,唯一区别是.ex文件会保留编译执行后产出的比特码文件, 63 | 而.exs文件用来作脚本执行,不会留下比特码文件。例如,如下创建名为math.exs的文件: 64 | ```elixir 65 | defmodule Math do 66 | def sum(a, b) do 67 | a + b 68 | end 69 | end 70 | 71 | IO.puts Math.sum(1, 2) 72 | ``` 73 | 74 | 执行之: 75 | ```sh 76 | $ elixir math.exs 77 | ``` 78 | 像这样执行脚本文件时,将在内存中编译和执行,打印出“3”作为结果。没有比特码文件生成。 79 | 后文中(为了学习和练习方便),推荐使用脚本模式执行学到的代码。 80 | 81 | ## 8.3-命名函数 82 | 在某模块中,我们可以用```def/2```宏定义函数,用```defp/2```定义私有函数。 83 | 用```def/2```定义的函数可以被其它模块中的代码使用,而私有函数仅在定义它的模块内使用。 84 | ```elixir 85 | defmodule Math do 86 | def sum(a, b) do 87 | do_sum(a, b) 88 | end 89 | 90 | defp do_sum(a, b) do 91 | a + b 92 | end 93 | end 94 | 95 | Math.sum(1, 2) #=> 3 96 | Math.do_sum(1, 2) #=> ** (UndefinedFunctionError) 97 | ``` 98 | 99 | 函数声明也支持使用卫兵或多个子句。 100 | 如果一个函数有好多子句,Elixir会匹配每一个子句直到找到一个匹配的。 101 | 下面例子检查参数是否是数字: 102 | ```elixir 103 | defmodule Math do 104 | def zero?(0) do 105 | true 106 | end 107 | 108 | def zero?(x) when is_number(x) do 109 | false 110 | end 111 | end 112 | 113 | Math.zero?(0) #=> true 114 | Math.zero?(1) #=> false 115 | 116 | Math.zero?([1,2,3]) 117 | #=> ** (FunctionClauseError) 118 | ``` 119 | 如果没有一个子句能匹配参数,会报错。 120 | 121 | ## 8.4-函数捕捉 122 | 本教程中提到函数,都是用```name/arity```的形式描述。 123 | 这种表示方法可以被用来获取一个命名函数(赋给一个函数型变量)。 124 | 下面用iex执行一下上文定义的math.exs文件: 125 | ```elixir 126 | $ iex math.exs 127 | ``` 128 | 129 | ```elixir 130 | iex> Math.zero?(0) 131 | true 132 | iex> fun = &Math.zero?/1 133 | &Math.zero?/1 134 | iex> is_function fun 135 | true 136 | iex> fun.(0) 137 | true 138 | ``` 139 | 用```&```通过函数名捕捉一个函数,它本身代表该函数值(函数类型的值)。 140 | 它可以不必赋给一个变量,直接用括号来使用该函数。 141 | 142 | 本地定义的,或者已导入的函数,比如```is_function/1```,可以不用前缀模模块名: 143 | ```elixir 144 | iex> &is_function/1 145 | &:erlang.is_function/1 146 | iex> (&is_function/1).(fun) 147 | true 148 | ``` 149 | 150 | 这种语法还可以作为快捷方式来创建和使用函数: 151 | ```elixir 152 | iex> fun = &(&1 + 1) 153 | #Function<6.71889879/1 in :erl_eval.expr/5> 154 | iex> fun.(1) 155 | 2 156 | ``` 157 | 158 | 代码中```&1``` 表示传给该函数的第一个参数。 159 | 因此,```&(&1+1)```其实等同于```fn x->x+1 end```。在创建短小函数时,这个很方便。 160 | 想要了解更多关于```&```捕捉操作符,参考[Kernel.SpecialForms文档](http://elixir-lang.org/docs/stable/elixir/Kernel.SpecialForms.html)。 161 | 162 | ## 8.5-默认参数 163 | Elixir中,命名函数也支持默认参数: 164 | ```elixir 165 | defmodule Concat do 166 | def join(a, b, sep \\ " ") do 167 | a <> sep <> b 168 | end 169 | end 170 | 171 | IO.puts Concat.join("Hello", "world") #=> Hello world 172 | IO.puts Concat.join("Hello", "world", "_") #=> Hello_world 173 | ``` 174 | 175 | 任何表达式都可以作为默认参数,但是只在函数调用时 **用到了** 才被执行。 176 | (函数定义时,那些表达式只是存在那儿,不执行;函数调用时,没有用到默认值,也不执行)。 177 | ```elixir 178 | defmodule DefaultTest do 179 | def dowork(x \\ IO.puts "hello") do 180 | x 181 | end 182 | end 183 | ``` 184 | 185 | ```elixir 186 | iex> DefaultTest.dowork 123 187 | 123 188 | iex> DefaultTest.dowork 189 | hello 190 | :ok 191 | ``` 192 | 193 | 如果有默认参数值的函数有了多条子句,推荐先定义一个函数头(无具体函数体)声明默认参数: 194 | ```elixir 195 | defmodule Concat do 196 | def join(a, b \\ nil, sep \\ " ") 197 | 198 | def join(a, b, _sep) when is_nil(b) do 199 | a 200 | end 201 | 202 | def join(a, b, sep) do 203 | a <> sep <> b 204 | end 205 | end 206 | 207 | IO.puts Concat.join("Hello", "world") #=> Hello world 208 | IO.puts Concat.join("Hello", "world", "_") #=> Hello_world 209 | IO.puts Concat.join("Hello") #=> Hello 210 | ``` 211 | 212 | 使用默认值时,注意对函数重载会有一定影响。考虑下面例子: 213 | ```elixir 214 | defmodule Concat do 215 | def join(a, b) do 216 | IO.puts "***First join" 217 | a <> b 218 | end 219 | 220 | def join(a, b, sep \\ " ") do 221 | IO.puts "***Second join" 222 | a <> sep <> b 223 | end 224 | end 225 | ``` 226 | 227 | 如果将以上代码保存在文件“concat.ex”中并编译,Elixir会报出以下警告: 228 | ```elixir 229 | concat.ex:7: this clause cannot match because a previous clause at line 2 always matches 230 | ``` 231 | 232 | 编译器是在警告我们,在使用两个参数调用```join```函数时,总使用第一个函数定义。 233 | 只有使用三个参数调用时,才会使用第二个定义: 234 | 235 | ```elixir 236 | $ iex concat.exs 237 | ``` 238 | 239 | ```elixir 240 | iex> Concat.join "Hello", "world" 241 | ***First join 242 | "Helloworld" 243 | iex> Concat.join "Hello", "world", "_" 244 | ***Second join 245 | "Hello_world" 246 | ``` 247 | 248 | 后面几章将介绍使用命名函数来做循环,如何从别的模块中导入函数,以及模块的属性等。 249 | -------------------------------------------------------------------------------- /9-recursion.md: -------------------------------------------------------------------------------- 1 | 9-递归 2 | ====== 3 | 因为在Elixir中(或所有函数式语言中),数据有不变性(immutability),因此在写循环时与传统的命令式(imperative)语言有所不同。 4 | 例如某命令式语言的循环可以这么写: 5 | ``` 6 | for(i = 0; i < array.length; i++) { 7 | array[i] = array[i] * 2 8 | } 9 | ``` 10 | 11 | 上面例子中,我们改变了```array```,以及辅助变量```i```的值。这在Elixir中是不可能的。 12 | 尽管如此,函数式语言却依赖于某种形式的循环---递归:一个函数可以不断地被递归调用,直到某条件满足才停止。 13 | 考虑下面的例子:打印一个字符串若干次: 14 | ``` 15 | defmodule Recursion do 16 | def print_multiple_times(msg, n) when n <= 1 do 17 | IO.puts msg 18 | end 19 | 20 | def print_multiple_times(msg, n) do 21 | IO.puts msg 22 | print_multiple_times(msg, n - 1) 23 | end 24 | end 25 | 26 | Recursion.print_multiple_times("Hello!", 3) 27 | # Hello! 28 | # Hello! 29 | # Hello! 30 | ``` 31 | 32 | 一个函数可以有许多子句(上面看起来定义了两个函数,但卫兵条件不同,可以看作同一个函数的两个子句)。 33 | 当参数匹配该子句的模式,且该子句的卫兵表达式返回true,才会执行该子句内容。 34 | 上面例子中,当```print_multiple_times/2```第一次调用时,n的值是3。 35 | 36 | 第一个子句有卫兵表达式要求n必须小于等于1。因为不满足此条件,代码找该函数的下一条子句。 37 | 38 | 参数匹配第二个子句,且该子句也没有卫兵表达式,因此得以执行。 39 | 首先打印```msg```,然后调用自身并传递第二个参数```n-1```(=2)。 40 | 这样```msg```又被打印一次,之后调用自身并传递参数```n-1```(=1)。 41 | 42 | 这个时候,n满足第一个函数子句条件。遂执行该子句,打印```msg```,然后就此结束。 43 | 44 | 我们称例子中第一个函数子句这种子句为“基本情形”。 45 | 基本情形总是最后被执行,因为起初通常都不匹配执行条件,程序而转去执行其它子句。 46 | 但是,每执行一次其它子句,条件都向这基本情形靠拢一点,直到最终回到基本情形处执行代码。 47 | 48 | 下面我们使用递归的威力来计算列表元素求和: 49 | ``` 50 | defmodule Math do 51 | def sum_list([head|tail], accumulator) do 52 | sum_list(tail, head + accumulator) 53 | end 54 | 55 | def sum_list([], accumulator) do 56 | accumulator 57 | end 58 | end 59 | 60 | Math.sum_list([1, 2, 3], 0) #=> 6 61 | ``` 62 | 63 | 我们首先用列表[1,2,3]和初值0作为参数调用函数,程序将逐个匹配各子句的条件,执行第一个符合要求的子句。 64 | 于是,参数首先满足例子中定义的第一个子句。参数匹配使得head = 1,tail = [2,3],accumulator = 0。 65 | 66 | 然后执行该字据内容,把head + accumulator作为第二个参数,连带去掉了head的列表做第一个参数,再次调用函数本身。 67 | 如此循环,每次都把新传入的列表的head加到accumulator上,传递去掉了head的列表。 68 | 最终传递的列表为空,符合第二个子句的条件,执行该子句,返回accumulator的值6。 69 | 70 | 几次函数调用情况如下: 71 | ``` 72 | sum_list [1, 2, 3], 0 73 | sum_list [2, 3], 1 74 | sum_list [3], 3 75 | sum_list [], 6 76 | ``` 77 | 78 | 这种使用列表做参数,每次削减一点列表的递归方式,称为“递减”算法,是函数式编程的核心。 79 |
80 | 如果我们想给每个列表元素加倍呢?: 81 | ``` 82 | defmodule Math do 83 | def double_each([head|tail]) do 84 | [head * 2| double_each(tail)] 85 | end 86 | 87 | def double_each([]) do 88 | [] 89 | end 90 | end 91 | 92 | Math.double_each([1, 2, 3]) #=> [2, 4, 6] 93 | ``` 94 | 95 | 此处使用了递归来遍历列表元素,使它们加倍,然后返回新的列表。 96 | 这样以列表为参数,递归处理其每个元素的方式,称为“映射(map)”算法。 97 |
98 | 99 | 递归和列尾调用优化(tail call optimization)是Elixir中重要的部分,通常用来创建循环。 100 | 尽管如此,在Elixir中你很少会使用以上方式来递归地处理列表。 101 | 102 | 下一章要介绍的[Enum模块](http://elixir-lang.org/docs/stable/elixir/Enum.html)为操作列表提供了诸多方便。 103 | 比如,下面例子: 104 | ``` 105 | iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end) 106 | 6 107 | iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end) 108 | [2, 4, 6] 109 | ``` 110 | 111 | 或者,使用捕捉的语法: 112 | ``` 113 | iex> Enum.reduce([1, 2, 3], 0, &+/2) 114 | 6 115 | iex> Enum.map([1, 2, 3], &(&1 * 2)) 116 | [2, 4, 6] 117 | ``` 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Elixir编程入门 2 | ============= 3 | 4 | Elixir,[ɪ'lɪksər],意为灵丹妙药、圣水,其logo是一枚紫色水滴: 5 | 6 | ![logo](http://elixir-lang.org/images/logo/logo.png) 7 | 8 | Elixir是一门建立在Erlang虚拟机上的[**函数式**](http://baike.baidu.com/view/3476448.htm?fr=aladdin)系统编程语言,支持元编程。 9 | 创始人[José Valim](https://github.com/josevalim)是ruby界的知名人士。 10 | 可以把Elixir看作函数式的ruby语言,或者是语法类似ruby的Erlang。 11 | Elixir受瞩目的主要原因,是因为它较好地结合了Erlang编程语言的各种优点,以及ruby那样简单易懂的语法(Erlang语法比较晦涩)。 12 | 13 | Elixir还是一门初出茅庐的语言: 14 | - 2014年9月1日临晨,1.0.0rc1发布 15 | - 2014年9月7日晚,1.0.0rc2发布 16 | - 2014年9月18日,[v1.0正式发布](http://elixir-lang.org/blog/2014/09/18/elixir-v1-0-0-released/) 17 | - 2015年9月28日,[v1.1发布](http://elixir-lang.org/blog/2015/09/28/elixir-v1-1-0-released/) 18 | - 2016年1月1日,[v1.2发布](http://elixir-lang.org/blog/2016/01/03/elixir-v1-2-0-released/) 19 | - 2016年6月2日,[v1.3发布](http://elixir-lang.org/blog/2016/06/21/elixir-v1-3-0-released/) 20 | 21 | 本文主要框架为Elixir官方的入门教程,辅以网上其它Elixir资源的内容,以及花钱:sob:购买的原版书籍(Dave Thomas的《Programming Elixir》,Progmatic) 22 | 23 | >请帮助更新文档(发个pr)。讨论问题可发issue。 24 | 25 | **基本教程** 26 | 27 | [1-简介](../master/1-intro.md)
28 | [2-基本数据类型](../master/2-basic-types.md)
29 | [3-基本运算符](../master/3-basic-ops.md)
30 | [4-模式匹配](../master/4-pattern-matching.md)
31 | [5-流程控制](../master/5-case-cond-if.md)
32 | [6-二进制串-字符串-字符列表](../master/6-bin-str-charlist.md)
33 | [7-键值-图-字典](../master/7-keywords-map-dict.md)
34 | [8-模块](../master/8-modules.md)
35 | [9-递归](../master/9-recursion.md)
36 | [10-枚举类型和流](../master/10-enum-stream.md)
37 | [11-进程](../master/11-process.md)
38 | [12-IO和文件系统](../master/12-io.md)
39 | [13-别名和程序导入](../master/13-alias-req-imp.md)
40 | [14-模块属性](../master/14-mod-attr.md)
41 | [15-结构体](../master/15-structs.md)
42 | [16-协议](../master/16-proto.md)
43 | [17-异常处理](../master/17-try-catch.md)
44 | [18-列表速构](../master/18-comprehensions.md)
45 | [19-魔法印](../master/19-sigils.md)
46 | [20-Typespecs和behaviors](../master/20-typespecs-behaviors.md)
47 | [21-Erlang库](../master/21-erlang-lib.md)
48 | [22-下一步](../master/22-next.md)
49 | 50 | 51 | 52 | **偷偷写在后面的话**   53 | Elixir处于蓬勃发展中,现在学习的ROI比较高;请谨慎、小规模用于生产环境。 54 | -------------------------------------------------------------------------------- /cheat.ex: -------------------------------------------------------------------------------- 1 | # Github会以为本repo是elixir 100% 2 | --------------------------------------------------------------------------------