学习一门新的编程语言可能是一种挑战。经常会有大量的新的代码模式、技术和功能。
尤其是Elixir,它有点奇怪。虽然语法看起来很简单,但即使你是一个函数式编程的老手,Elixir代码有时也是一个难题。
在这篇文章中,我浏览了所有基本的Elixir结构,并收集了一些提示,这些提示将帮助你写出成语式的Elixir代码,而不需要花几个小时去搜索。当你尝试你的第一个Elixir项目或做Exercism练习时,它应该是有用的。
如果你已经完成了Exercism或做了Elixir课程,你会知道其中大部分内容,但它可以作为一个很好的复习。
现在,让我们看看Elixir能给我们带来什么。 😉
🥑 基本语法
要学习基本语法,只需看这个视频。它做得很好,令人惊叹。
此外,你可以访问Elixir网页上的基本类型和基本操作符指南。
🔧管道
.png)
跳进管道,它们会把你带到奇怪的地方。
管道操作符是Elixir中函数组合的主要操作符。
它接收前面表达式的结果,并将其作为下面表达式的第一个参数。foo |> bar |> baz 管道取代了嵌套的函数调用,比如用foo(bar(baz))。
管道的例子
例如,这里有一个函数将一个字符串变成一个感叹句:
def exclaim(string) do
string
|> String.trim()
|> String.capitalize()
|> Kernel.<>("!")
end
在这里,我们取一个字符串,并通过各种字符串方法传递给它。由于Elixir函数返回最后一个评估值的结果,所以我们不需要返回任何东西。数据和应用于它的转换被清楚地划分出来。
当你用管道来组成更高层次的函数时,管道看起来更加不可思议。整个程序可以很容易地成为一个大管道,由更小的管道组成,而后者又由更小的管道组成。一路走来的管道
➡️高阶函数
Elixir作为一种函数式编程语言,大量使用了高阶函数。
高阶函数将其他函数作为参数。高阶函数的几个例子是map、filter和reduce。
在Elixir中,这些函数大部分都存在于标准库的Enum模块中,并对任何*可枚举*的东西进行操作:列表、地图、范围等。
使用HOF可以很好地替代循环:用Enum.map 代替for-in,遍历列表中的元素:
iex(1)> list = [1, 3, 5, 7]
[1, 3, 5, 7]
iex(2)> Enum.map(list, fn x -> x * x end)
[1, 9, 25, 49]
在上面的函数中,我们提供了平方函数,即fn x -> x * x end 。这就是Elixir的匿名函数的语法。还有另一种提供函数的方法,那就是通过*捕获操作符*。
iex(3)> Enum.map(list, &(&1 * &1))
[1, 9, 25, 49]
& 启动一个新的匿名函数,而&1 表示该函数的第一个参数。在这种情况下,是否能让人更容易读懂,是值得商榷的。😅
高阶函数的例子
假设我们需要从一个文件中读取一个行的列表,把它们变成感叹句,然后打印到控制台。
虽然我们通常可以用一个循环来做,但现在我们需要另一种方法。你可以用map函数轻松做到这一点:
def exclaimify(file) do
file
|> File.read!()
|> String.split("\n", trim: true)
|> Enum.map(fn x -> exclaim(x) end)
|> Enum.join("\n")
|> IO.puts()
end
还记得我们谈到的捕获操作符吗?在这里,你可以用它来使调用exclaim 。只要把地图函数换成Enum.map(&exclaim/1)!
🕵️模式匹配
.png)
模式匹配是Elixir中最强大的部分之一。你写的任何一种控制流,都可以通过合理的启发式方法来检查你是否不能用模式匹配来完成。
基本上,模式匹配的工作原理是提供一些你在数据中期望的模式,然后检查这个模式是否与数据匹配。
这一切都从匹配运算符开始,= 。是的。在Elixir中,我们不分配变量;我们匹配它们:
iex(1)> x = 1
1
这里发生的是,Elixir检查双方的结构是否相同。如果是的话,左边的未分配变量就会被分配到右边的值。如果不是,它将跳到下一个匹配语句(如果匹配不是详尽的,则给你MatchError)。
我们可以在IEx中玩一玩:
iex(2)> 1 = 1
1
iex(3)> 1 = 2
** (MatchError) no match of right hand side value: 2
iex(5)> x = 1
1
当结构更加复杂时,它开始变得更加有用,使你能够非常容易地解除图元、列表和嵌套图的结构。
元组
iex(1)> {x, y} = {1, 3}
{1, 3}
iex(2)> x
1
iex(3)> y
3
iex(1)> {x, y} = {1, 2, 3}
** (MatchError) no match of right hand side value: {1, 2, 3}
列表
iex(1)> [head | tail] = ["Many", "years", "later", "as", "he", "faced", "the", "firing", "squad"]
["Many", "years", "later", "as", "he", "faced", "the", "firing", "squad"]
iex(2)> head
"Many"
iex(3)> tail
["years", "later", "as", "he", "faced", "the", "firing", "squad"]
iex(4)> [head | tail] = []
** (MatchError) no match of right hand side value: []
地图
iex(1)> %{age: x} = %{name: "Andrew", age: 27}
%{age: 27, name: "Andrew"}
iex(2)> x
27
Elixir的case语句是基于模式匹配的--你可以用它来匹配值或结构。
def get_andrews_age(person) do
case person do
%{name: "Andrew", age: x} ->
{:ok, x}
%{name: _, age: _} ->
{:error, :not_andrew}
_ ->
{:error, :not_person}
end
end
在上面的代码中,我们可以更习惯性地去做。
多个函数条款
Elixir支持在函数子句中进行模式匹配。因此,你可以创建多个函数,而不是写冗长的控制流语句。
def get_andrews_age(%{name: "Andrew", age: x}), do: {:ok, x}
def get_andrews_age(%{name: _, age: _}), do: {:error, :not_andrew}
def get_andrews_age(_), do: {:error, :not_person}
喔!🤯如果操作得当,这将使代码更具扩展性,更容易阅读。
守护
但还有更多的模块化正在到来。
让我们想象一下,我们只想在安德鲁达到法定年龄时才得到年龄。我们可以用一个卫兵来轻松实现这个目标,卫兵是一种增强模式匹配的额外结构。
守护由when 和一个布尔表达式组成,并被添加到函数子句中。它们只支持一组有限的表达式,你可以在文档中查看。
下面是我们的代码在使用guards后的样子。
def get_andrews_age_if_legal(%{name: "Andrew", age: x}) when x >= 18, do: {:ok, x}
def get_andrews_age_if_legal(%{name: "Andrew", age: x}) when x < 18, do: {:error, :not_legal_age}
def get_andrews_age_if_legal(%{name: _, age: _}), do: {:error, :not_andrew}
def get_andrews_age_if_legal(_), do: {:error, :not_person}
重载函数
Elixir函数有一个很酷的东西,你可以有不同参数数的函数,它们被算作不同的函数,尽管它们有相同的名字。换句话说,你可以通过arity来重载函数。
def add(x,y), do: x + y
def add(x,y,z), do: x + y + z
用Elixir的话说,其中一个函数实际上是add/2,另一个是add/3。
而且,通过使用多个函数和模式匹配,我们可以在Elixir代码中做一个很好的技巧,这个技巧经常出现。
我们可以定义一个x-argument函数,作为我们函数的外壳。然后,我们用这个x-argument函数来启动一个x+1-argument函数,这个函数将跟踪状态作为一个参数,而不是一个变量。
如果你看一下它的动作,就更容易了。这里有一个可以反转列表的函数。
def reverse(list), do: reverse(list, [])
def reverse([], list), do:
def reverse([head | tail], reversed_list), do:
我没有完成实现的细节,你可能对正在发生的事情有一两个疑问。请跟我呆一会儿。为了充分解释它,我们需要转向递归函数。
➰ 递归
.png)
来源:xkcd
递归函数只是一个可以自我调用的函数。
其中一个典型的例子是计算某个斐波那契数,在Elixir中的一个天真实现是。
def fib(0), do: 0
def fib(1), do: 1
def fib(n), do: fib(n-1) + fib(n-2)
(注意我们上面提到的模式匹配的使用。)
我们可以在每段正常的递归代码中找到两样东西:一个基本情况和一个递归调用:
- 基本情况:这就是递归终止的地方。这是你想要达到的结果,而且它不会自己调用。
- 递归调用:这是程序将进行大部分计算的地方。有了它,你将把你的任务减少到一个更小的任务,而且它将调用自己。
基本上,你可以把基本情况看作是你想解决的任务的最简单的例子,而递归调用是简化任何给定问题的方法。在给定的例子中,fib(0) 和fib(1) 是基例,fib(n) 是递归调用。
循环与递归
在Elixir中,我们比在非FP语言中更频繁地使用递归,因为我们没有机会使用像循环这样的东西。递归很好地模仿了指令性行为。
例如,如果我们需要做一定次数的事情,我们可以用一个数字作为参数创建一个递归函数。
比方说,我们想把一个字符串复制任意给定的次数。
def repeat(string, times), do: repeat(string, times, "")
def repeat(_string, 0, acc), do: acc
def repeat(string, times, acc), do: repeat(string, times - 1, acc <> string)
我们不在函数中初始化一个变量,而是在函数中增加一个参数。然后我们一次又一次地调用它,每次都减少times ,增加acc 。
最后,我们取得了与实例化两个变量--i 和acc --并创建一个 while 循环,在每次迭代中递减i 直到等于 0,并将string 添加到acc 的相同结果。
现在,我相信你会发现很容易完成模式匹配部分的reverse 例子;它的结构与repeat 比较相似。
🏠 Structs
Structs是Elixir默认的键值存储的升级版,即maps。与map不同的是,结构体的键值在初始化时被设置,之后就不能再添加新的键值。
每个模块只能有一个结构,而且我们倾向于在模块的顶部定义它:
defmodule Person do
defstruct [:name, :age]
end
在上面的例子中,我现在可以用%Person{} 访问该结构。我还可以提供一些默认值。例如,我们假设我所处理的默认人口,由于某种原因,是27岁的安德鲁:
defmodule Person do
defstruct [name: "Andrew", age: 27]
end
现在,调用该结构会给我一个带有默认值的命名地图:
iex(1)> %Person{}
%Person{age: 27, name: "Andrew"}
结构的键一旦定义就不能改变。因此,你无法向Person结构添加额外的事实,比如家庭住址、最喜欢的陀思妥耶夫斯基小说,以及其他没有人需要知道的琐事。
但是你可以很容易地用默认值以外的值来实例化一个结构,用常规的映射函数更新这些值,等等。
您可以在这里阅读更多关于结构的信息。
使用结构
在Elixir中创建一个简单的游戏时,您可以,例如,创建一个编码游戏状态的结构。然后,你在下面的模块中提供修改游戏状态的函数。
Exercism在机器人模拟器练习中有一个精彩的例子。在其中,你创建了一个结构来保存机器人的位置和方向,并提供各种函数来移动和转动机器人。
可以说,像这样创建的模块,在某种意义上,功能类似于一个类,减去了所有继承的东西(我们在Elixir中没有这样做,但也有办法实现这样的行为)。
🗺️接下来要学什么?
这些知识(再加上一点Google搜索,我肯定没有涵盖所有的内容)应该足以让你通过Exercism,能够在Elixir中做简单的程序。
如果你想在这之后继续学习,我们有一篇关于你可以使用的所有不同资源的大文章。在实践方面,我建议为你感兴趣的东西建立一些玩具项目,也许可以研究一下Phoenix,Elixir的主要网络框架。我的意思是,你更了解你自己。 😉
此外,如果你想阅读更多关于Elixir和其他奇妙语言的文章,请务必在Twitter上关注我们。祝您好运!