Erlang/Elixir语法速成
lbjfish
7年前
<h2>Erlang/Elixir语法速成</h2> <p>本文是针对Erlang开发人员的Elixir语法简介,同时也适用于试图了解Erlang的Elixir开发者。</p> <p>本文简要介绍了Elixir/Erlang语法,互操作能力,在线文档和示例代码等最基础的知识。</p> <h2>1 运行代码</h2> <h3>1.1 Erlang</h3> <p>最快速运行代码的方法是启动Erlang shell - erl 。本文中大部分代码可以直接粘贴到shell中,</p> <p>Erlang中的命名函数必须包含在模块中,而且必须要在模块编译后才能使用。下面是一个模块的例子:</p> <pre> <code class="language-erlang">% module_name.erl -module(module_name). % you may use some other name -compile(export_all). hello() -> io:format("~s~n", ["Hello world!"]).</code></pre> <p>编辑文件并保存,在同一目录下运行 erl ,并执行 编译 命令</p> <pre> <code class="language-erlang">Eshell V5.9 (abort with ^G) 1> c(module_name). ok 1> module_name:hello(). Hello world! ok</code></pre> <p>shell运行的同时也可以编辑文件。 但不要忘记执行 c(module_name) 来加载最新的更改。</p> <p>要注意,文件名必须与在 -module() 中声明的文件名保持一致,扩展名为 .erl 。</p> <h3>1.2 Elixir</h3> <p>与Erlang类似,Elixir有一个名为 iex 的交互式shell。编译Elixir代码可以使用 elixirc (类似于Erlang的 erlc )。</p> <p>Elixir还提供一个名为 elixir 的可执行文件来运行Elixir代码。上面的模块用Elixir来写就是这样:</p> <pre> <code class="language-erlang"># module_name.ex defmodule ModuleName do def hello do IO.puts "Hello World" end end</code></pre> <p>然后,在 iex 中编译:</p> <pre> <code class="language-erlang">Interactive Elixir iex> c("module_name.ex") [ModuleName] iex> ModuleName.hello Hello world! :ok</code></pre> <p>要注意的是,在Elixir中,不要求模块必须保存在文件中,Elixir的模块可以直接在shell中定义:</p> <pre> <code class="language-erlang">defmodule MyModule do def hello do IO.puts "Another Hello" end end</code></pre> <h2>2 显著差异</h2> <p>这一节讨论了两种语言之间的一些语法差异。</p> <h3>2.1 操作符名称</h3> <p>部分操作符采用了不同的书写方式</p> <table> <thead> <tr> <th>ERLANG</th> <th>ELIXIR</th> <th>含义</th> </tr> </thead> <tbody> <tr> <td>and</td> <td>不可用</td> <td>逻辑‘与’,全部求值</td> </tr> <tr> <td>andalso</td> <td>and</td> <td>逻辑‘与’,采用短路求值策略</td> </tr> <tr> <td>or</td> <td>不可用</td> <td>逻辑‘或’,全部求值</td> </tr> <tr> <td>orelse</td> <td>or</td> <td>逻辑‘与’,采用短路求值策略</td> </tr> <tr> <td>=:=</td> <td>===</td> <td>全等</td> </tr> <tr> <td>=/=</td> <td>!==</td> <td>不全等</td> </tr> <tr> <td>/=</td> <td>!=</td> <td>不等于</td> </tr> <tr> <td>=<</td> <td><=</td> <td>小于等于</td> </tr> </tbody> </table> <h3>2.2 分隔符</h3> <p>Erlang表达式使用点号 . 作为结束,逗号 , 用来分割同一上下文中的多个表达式(例如在函数定义中)。</p> <p>在Elixir中,表达式由换行符或分号 ; 分隔。</p> <p>Erlang</p> <pre> <code class="language-erlang">X = 2, Y = 3. X + Y.</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">x = 2; y = 3 x + y</code></pre> <h3>2.3 变量名</h3> <p>Erlang中的变量只能被绑定一次,Erlang shell提供一个特殊的命令 f ,用于删除某个变量或所有变量的绑定。</p> <p>Elixir允许变量被多次赋值,如果希望匹配变量之前的值,应当使用 ^ 。</p> <p>Erlang</p> <pre> <code class="language-erlang">Eshell V5.9 (abort with ^G) 1> X = 10. 10 2> X = X + 1. ** exception error: no match of right hand side value 11 3> X1 = X + 1. 11 4> f(X). ok 5> X = X1 * X1. 121 6> f(). ok 7> X. * 1: variable 'X' is unbound 8> X1. * 1: variable 'X1' is unbound</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">iex> a = 1 1 iex> a = 2 2 iex> ^a = 3 ** (MatchError) no match of right hand side value: 3</code></pre> <h3>2.4 函数调用</h3> <p>Elixir允许在函数调用中省略括号,而Erlang不允许。</p> <table> <thead> <tr> <th>ERLANG</th> <th>ELIXIR</th> </tr> </thead> <tbody> <tr> <td>some_function().</td> <td>some_function</td> </tr> <tr> <td>sum(A, B)</td> <td>sum a, b</td> </tr> </tbody> </table> <p>调用模块中的函数的语法不同,Erlang中,下面的代码</p> <pre> <code class="language-erlang">lists : last ([ 1 , 2 ]).</code></pre> <p>表示从 list 模块中调用函数 last ,而在Elixir中,使用点号 . 代替冒号 :</p> <pre> <code class="language-erlang">List.last([1, 2])</code></pre> <p>注意在Elixir中,由于Erlang的模块用原子表示,因此用如下方法调用Erlang的函数:</p> <pre> <code class="language-erlang">:lists.sort [3, 2, 1]</code></pre> <p>所有Erlang的内置函数都包含在 :erlang 模块中</p> <h2>3 数据类型</h2> <p>Erlang和Elixir的数据类型大部分都相同,但依然有一些差异。</p> <h3>3.1 原子</h3> <p>Erlang中, 原子 是以小写字母开头的任意标志符,例如 ok 、 tuple 、 donut .</p> <p>以大写字母开头的标识符则会被视为变量名。而在Elixir中,前者被用于变量名,而后者则被视为原子的别名。</p> <p>Elixir中的原子始终以冒号 : 作为首字符。</p> <p>Erlang</p> <pre> <code class="language-erlang">im_an_atom. me_too. Im_a_var. X = 10.</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">:im_an_atom :me_too im_a_var x = 10 Module # 称为原子别名; 展开后是 :'Elixir.Module'</code></pre> <p>非小写字母开头的标识符也可以作为原子。 不过两种语言的语法有所不同:</p> <p>Erlang</p> <pre> <code class="language-erlang">is_atom(ok). %=> true is_atom('0_ok'). %=> true is_atom('Multiple words'). %=> true is_atom(''). %=> true</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">is_atom :ok #=> true is_atom :'ok' #=> true is_atom Ok #=> true is_atom :"Multiple words" #=> true is_atom :"" #=> true</code></pre> <h3>3.2 元组</h3> <p>两种语言元组的语法是相同的,不过API有所不同,Elixir尝试使用下面的方法规范化Erlang库:</p> <ol> <li>函数的 subject 总是作为第一个参数。</li> <li>所有对数据结构的操作均基于零进行。</li> </ol> <p>也就是说,Elixir不会导入默认的 element 和 setelement 函数,而是提供 elem 和 put_elem 作为替代:</p> <p>Erlang</p> <pre> <code class="language-erlang">element(1, {a, b, c}). %=> a setelement(1, {a, b, c}, d). %=> {d, b, c}</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">elem({:a, :b, :c}, 0) #=> :a put_elem({:a, :b, :c}, 0, :d) #=> {:d, :b, :c}</code></pre> <h3>3.3 列表与二进制串</h3> <p>Elixir具有访问二进制串的快捷语法:</p> <p>Erlang</p> <pre> <code class="language-erlang">is_list('Hello'). %=> false is_list("Hello"). %=> true is_binary(<<"Hello">>). %=> true</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">is_list 'Hello' #=> true is_binary "Hello" #=> true is_binary <<"Hello">> #=> true <<"Hello">> === "Hello" #=> true</code></pre> <p>Elixir中, <strong>字符串</strong> 意味着一个UTF-8编码的二进制串, String 模块可用于处理字符串。</p> <p>同时Elixir也希望程序源码采用UTF-8编码。而在Erlang中, <strong>字符串</strong> 表示字符的列表,</p> <p>:string 模块用于处理它们,但并没有采用UTF-8编码。</p> <p>Elixir还支持多行字符串(也被称为 <em>heredocs</em> ):</p> <pre> <code class="language-erlang">is_binary """ This is a binary spanning several lines. """ #=> true</code></pre> <h3>3.4 关键字列表(Keyword list)</h3> <p>Elixir中,如果列表是由具有两个元素的元组组成,并且每个元组中的第一个元素是原子,则称这样的列表为关键字列表:</p> <p>Erlang</p> <pre> <code class="language-erlang">Proplist = [{another_key, 20}, {key, 10}]. proplists:get_value(another_key, Proplist). %=> 20</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">kw = [another_key: 20, key: 10] kw[:another_key] #=> 20</code></pre> <h3>3.5 映射(Map)</h3> <p>Erlang R17中引入了映射,一种无序的键-值数据结构。键和值可以是任意的数据类型,</p> <p>映射的创建、更新和模式匹配如下所示:</p> <p>Erlang</p> <pre> <code class="language-erlang">Map = #{key => 0}. Updated = Map#{key := 1}. #{key := Value} = Updated. Value =:= 1. %=> true</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">map = %{:key => 0} map = %{map | :key => 1} %{:key => value} = map value === 1 #=> true</code></pre> <p>当键为原子时,Elixir可以使用 key: 0 来定义映射,使用 .key 来访问值:</p> <pre> <code class="language-erlang">map = %{key: 0} map = %{map | key: 1} map.key === 1</code></pre> <h3>3.6 正则表达式</h3> <p>Elixir支持正则表达式语法,允许在编译(elixir源码)时编译正则表达式,</p> <p>而不是等到运行时再进行编译。而且对于特殊的字符,也无需进行多次转义:</p> <p>Erlang</p> <pre> <code class="language-erlang">{ ok, Pattern } = re:compile("abc\\s"). re:run("abc ", Pattern). %=> { match, ["abc "] }</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">Regex.run ~r/abc\s/, "abc " #=> ["abc "]</code></pre> <p>支持在 <em>heredocs</em> 中书写正则,这样便于理解复杂正则</p> <p>Elixir</p> <pre> <code class="language-erlang">Regex.regex? ~r""" This is a regex spanning several lines. """ #=> true</code></pre> <h2>4 模块</h2> <p>每个Erlang模块都保存在与其同名的文件中,具有以下结构:</p> <pre> <code class="language-erlang">-module(hello_module). -export([some_fun/0, some_fun/1]). % A "Hello world" function some_fun() -> io:format('~s~n', ['Hello world!']). % This one works only with lists some_fun(List) when is_list(List) -> io:format('~s~n', List). % Non-exported functions are private priv() -> secret_info.</code></pre> <p>在这里,我们创建了一个名为 hello_module 的模块。模块中定义了三个函数,</p> <p>顶部的 export 指令导出了前两个函数,让它们能够被其他模块调用。 export 指令里包含了需要导出函数的列表,</p> <p>其中每个函数都写作 <函数名>/<元数> 的形式。在这里,元数表示函数参数的个数。</p> <p>和上述Erlang代码作用相同的Elixir代码:</p> <pre> <code class="language-erlang">defmodule HelloModule do # A "Hello world" function def some_fun do IO.puts "Hello world!" end # This one works only with lists def some_fun(list) when is_list(list) do IO.inspect list end # A private function defp priv do :secret_info end end</code></pre> <p>在Elixir中,一个文件中可以包含多个模块,并且还允许嵌套定义模块:</p> <pre> <code class="language-erlang">defmodule HelloModule do defmodule Utils do def util do IO.puts "Utilize" end defp priv do :cant_touch_this end end def dummy do :ok end end defmodule ByeModule do end HelloModule.dummy #=> :ok HelloModule.Utils.util #=> "Utilize" HelloModule.Utils.priv #=> ** (UndefinedFunctionError) undefined function: HelloModule.Utils.priv/0</code></pre> <h2>5 函数语法</h2> <p>「Learn You Some Erlang」书中的 <a href="/misc/goto?guid=4959756351835546370" rel="nofollow,noindex">这一章</a> 详细讲解了Erlang的模式匹配和函数语法。</p> <p>而本文只简要介绍主要内容并展示部分示例代码。</p> <h3>5.1 模式匹配</h3> <p>Elixir中的模式匹配基于于Erlang实现,两者通常非常类似:</p> <p>Erlang</p> <pre> <code class="language-erlang">loop_through([H | T]) -> io:format('~p~n', [H]), loop_through(T); loop_through([]) -> ok.</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">def loop_through([h | t]) do IO.inspect h loop_through t end def loop_through([]) do :ok end</code></pre> <p>当多次定义名称相同的函数时,每个这样的定义称为 <strong>子句</strong> 。</p> <p>在Erlang中,子句总是按顺序写在一起并使用分号 ; 分隔 。 最后一个子句用点号 . 结束。</p> <p>Elixir不需要通过符号来分隔子句,不过要求子句必须按顺序写在一起。</p> <h3>5.2 函数识别</h3> <p>在Erlang和Elixir中,仅凭函数名是无法区分一个函数的。必须通过函数名和元数加以区分。</p> <p>下面两个例子中,我们定义了四个不同的函数(所有名字都为 sum ,但它们具有不同的元数):</p> <p>Erlang</p> <pre> <code class="language-erlang">sum() -> 0. sum(A) -> A. sum(A, B) -> A + B. sum(A, B, C) -> A + B + C.</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">def sum, do: 0 def sum(a), do: a def sum(a, b), do: a + b def sum(a, b, c), do: a + b + c</code></pre> <p>Guard表达式提供了一种简明的方法来定义在不同条件下接受有限个数参数的函数。</p> <p>Erlang</p> <pre> <code class="language-erlang">sum(A, B) when is_integer(A), is_integer(B) -> A + B; sum(A, B) when is_list(A), is_list(B) -> A ++ B; sum(A, B) when is_binary(A), is_binary(B) -> <<A/binary, B/binary>>. sum(1, 2). %=> 3 sum([1], [2]). %=> [1, 2] sum("a", "b"). %=> "ab"</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">def sum(a, b) when is_integer(a) and is_integer(b) do a + b end def sum(a, b) when is_list(a) and is_list(b) do a ++ b end def sum(a, b) when is_binary(a) and is_binary(b) do a <> b end sum 1, 2 #=> 3 sum [1], [2] #=> [1, 2] sum "a", "b" #=> "ab"</code></pre> <h3>5.3 默认值</h3> <p>Elixir允许参数具有默认值,而Erlang不允许。</p> <pre> <code class="language-erlang">def mul_by(x, n \\ 2) do x * n end mul_by 4, 3 #=> 12 mul_by 4 #=> 8</code></pre> <h3>5.4 匿名函数</h3> <p>定义匿名函数:</p> <pre> <code class="language-erlang">Sum = fun(A, B) -> A + B end. Sum(4, 3). %=> 7 Square = fun(X) -> X * X end. lists:map(Square, [1, 2, 3, 4]). %=> [1, 4, 9, 16]</code></pre> <pre> <code class="language-erlang">sum = fn(a, b) -> a + b end sum.(4, 3) #=> 7 square = fn(x) -> x * x end Enum.map [1, 2, 3, 4], square #=> [1, 4, 9, 16]</code></pre> <p>定义匿名函数时也可以使用模式匹配。</p> <pre> <code class="language-erlang">F = fun(Tuple = {a, b}) -> io:format("All your ~p are belong to us~n", [Tuple]); ([]) -> "Empty" end. F([]). %=> "Empty" F({a, b}). %=> "All your {a, b} are belong to us"</code></pre> <pre> <code class="language-erlang">f = fn {:a, :b} = tuple -> IO.puts "All your #{inspect tuple} are belong to us" [] -> "Empty" end f.([]) #=> "Empty" f.({:a, :b}) #=> "All your {:a, :b} are belong to us"</code></pre> <h3>5.5 作为一等公民(first-class)的函数</h3> <p>匿名函数是 <em>first-class values</em> ,因此它们可以当作参数传递给其他函数,也可以被当作返回值。</p> <p>对于命名函数,可以使用如下语法实现上述功能。</p> <pre> <code class="language-erlang">-module(math). -export([square/1]). square(X) -> X * X. lists:map(fun math:square/1, [1, 2, 3]). %=> [1, 4, 9]</code></pre> <pre> <code class="language-erlang">defmodule Math do def square(x) do x * x end end Enum.map [1, 2, 3], &Math.square/1 #=> [1, 4, 9]</code></pre> <h3>5.6 Elixir中的局部应用与函数捕捉</h3> <p>Elixir可以利用函数的局部应用(partial application),以简洁的方式定义匿名函数:</p> <pre> <code class="language-erlang">Enum.map [1, 2, 3, 4], &(&1 * 2) #=> [2, 4, 6, 8] List.foldl [1, 2, 3, 4], 0, &(&1 + &2) #=> 10</code></pre> <p>函数捕捉同样使用 & 操作符,它使得命名函数可以作为参数传递。</p> <pre> <code class="language-erlang">defmodule Math do def square(x) do x * x end end Enum.map [1, 2, 3], &Math.square/1 #=> [1, 4, 9]</code></pre> <p>上面的代码相当于Erlang的 fun math:square/1 。</p> <h2>6. 流程控制</h2> <p>if 和 case 结构在Erlang和Elixir中实际上是表达式,不过依然可以像命令式语言的语句那样,用于流程控制</p> <h3>6.1 Case</h3> <p>case 结构是完全基于模式匹配的流程控制。</p> <p>Erlang</p> <pre> <code class="language-erlang">case {X, Y} of {a, b} -> ok; {b, c} -> good; Else -> Else end</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">case {x, y} do {:a, :b} -> :ok {:b, :c} -> :good other -> other end</code></pre> <h3>6.2 If</h3> <p>Erlang</p> <pre> <code class="language-erlang">Test_fun = fun (X) -> if X > 10 -> greater_than_ten; X < 10, X > 0 -> less_than_ten_positive; X < 0; X =:= 0 -> zero_or_negative; true -> exactly_ten end end. Test_fun(11). %=> greater_than_ten Test_fun(-2). %=> zero_or_negative Test_fun(10). %=> exactly_ten</code></pre> <p>Elixir</p> <pre> <code class="language-erlang">test_fun = fn(x) -> cond do x > 10 -> :greater_than_ten x < 10 and x > 0 -> :less_than_ten_positive x < 0 or x === 0 -> :zero_or_negative true -> :exactly_ten end end test_fun.(44) #=> :greater_than_ten test_fun.(0) #=> :zero_or_negative test_fun.(10) #=> :exactly_ten</code></pre> <p>Elixir的 cond 和Erlang的 if 有两个重要的区别:</p> <ul> <li>cond 允许左侧为任意表达式,而Erlang只允许Guard子句;</li> <li>cond 使用Elixir中的真值概念(除了 nil 和 false 皆为真值),而Erlang的 if 则严格的期望一个布尔值;</li> </ul> <p>Elixir同样提供了一个类似于命令式语言中 if 的功能,用于检查一个子句是true还是false:</p> <pre> <code class="language-erlang">if x > 10 do :greater_than_ten else :not_greater_than_ten end</code></pre> <h3>6.3 发送和接收消息</h3> <p>发送和接收消息的语法仅略有不同:</p> <pre> <code class="language-erlang">Pid = self(). Pid ! {hello}. receive {hello} -> ok; Other -> Other after 10 -> timeout end.</code></pre> <pre> <code class="language-erlang">pid = Kernel.self send pid, {:hello} receive do {:hello} -> :ok other -> other after 10 -> :timeout end</code></pre> <h2>7 将Elixir添加到已有的Erlang程序中</h2> <p>Elixir会被编译成BEAM字节码(通过Erlang抽象格式)。这意味着Elixir和Erlang的代码可以互相调用而不需要添加其他任何绑定。</p> <p>Erlang代码中使用Elixir模块须要以 Elixir. 作为前缀,然后将Elixir的调用附在其后。</p> <p>例如,这里演示了在Erlang中如何使用Elixir的 String 模块:</p> <pre> <code class="language-erlang">-module(bstring). -export([downcase/1]). downcase(Bin) -> 'Elixir.String':downcase(Bin).</code></pre> <h3>7.1 使用Rebar集成</h3> <p>如果使用rebar,应当把Elixir的git仓库引入并作为依赖添加:</p> <pre> <code class="language-erlang">https://github.com/elixir-lang/elixir.git</code></pre> <p>Elixir的结构与Erlang的OTP类似,被分为不同的应用放在 lib 目录下,可以在 <a href="/misc/goto?guid=4959616665865140638" rel="nofollow,noindex">Elixir源码仓库</a> 中看到这种结构。</p> <p>由于rebar无法识别这种结构,因此需要在 rebar.config 中明确的配置所需要的Elixir应用,例如:</p> <pre> <code class="language-erlang">{lib_dirs, [ "deps/elixir/lib" ]}.</code></pre> <p>这样就能直接从Erlang调用Elixir代码了,如果需要编写Elixir代码,还应安装 <a href="/misc/goto?guid=4959756351966934321" rel="nofollow,noindex">自动编译Elixir的rebar插件</a> 。</p> <h3>7.2 手动集成</h3> <p>如果不使用rebar,在已有Erlang软件中使用Elixir的最简单的方式是按照 <a href="/misc/goto?guid=4958977353683990572" rel="nofollow,noindex">入门指南</a> 中的方法安装Elixir,然后将 lib 添目录加到 ERL_LIBS 中。</p> <h2>8 扩展阅读</h2> <p>Erlang的官方文档网站有不错的编程 <a href="/misc/goto?guid=4959756352081309463" rel="nofollow,noindex">示例集</a> ,把它们重新用Elixir实现一遍是不错的练习方法。</p> <p><a href="/misc/goto?guid=4959756352171384999" rel="nofollow,noindex">Erlang cookbook</a> 也提供了更多有用的代码示例。</p> <p>还可以进一步阅读Elixir的 <a href="/misc/goto?guid=4958977353683990572" rel="nofollow,noindex">入门指南</a> 和 <a href="/misc/goto?guid=4959756352278944220" rel="nofollow,noindex">在线文档</a> 。</p> <p> </p> <p>来自:https://segmentfault.com/a/1190000012776435</p> <p> </p>