Elixir 基本语法
好久没学习,随便写点东西,一直想学FP来着,不过之前 Haskell 整的有点难受,好难啊不太会,下次静下心来好好学一学吧,不过先试试 Erlang / Elixir,听说也很难?
至于原因,莫名喜欢 Erlang,不知道为什么哈哈哈哈,得知有 Elixir 这个披着 Ruby 皮、用着 Beam 的 Lisp 觉得还不错?毕竟 Lisp 大法好!! (虽然我不会 lisp) 不过 Elixir 名字好听 Logo 也好看
好了,前置吐槽就这么多吧,希望可以静下心好好学学 Elixir,呃,我也不知道可不可以啦,但是如果对 Elixir 感兴趣的话可以在 Elixir School 尝试学习一下,我也才开始从这里开始学习
基本类型
-
整数类型:在 Erlang 和 Elixir 中,整数类型都是高精度类型,不区分类型所占的字节,有点类似 Python 中的整数 Elixir 支持 二(0b)、八(0o)、十、十六(0x)进制的整数字面量,使用起来十分方便
1 2 3 4
255 # 十进制整数 255 0b10001000 # 二进制整数 136 0o7654321 # 八进制整数 2054353 0xFFFF # 十六进制整数 65535
-
浮点类型:嗯,它是 IEEE 754,好了就这样吧,介绍完了
-
布尔类型:true 和 false,不过有一点需要注意,在 Elixir 中除了 false 和 nil 之外的所有值都为 true
-
原子类型:名字和代表的值相同的常量,有点像 Ruby 和 Lisp 中的 Symbol。BTW 布尔值 true / false 实际对应的是原子 :true 和 :false
1 2 3 4 5
:foo # 符号,名为 :foo :foo == :bar # false is_atom(true) # true is_boolean(:false) # true true == :true # true
大写字母开始的别名也是原子,模块名 (module) 也是原子,当然也可以使用原子直接引用 Erlang 标准库的模块
1 2
is_atom(MyAtom) # true is_atom(MyApp.MyModule) # true
-
字符串:这是一个 UTF-8 编码的字符串,用双引号包住,并且支持换行与转义字符
1 2
"Hello" # "Hello" "这是一个\nString" #这是一个\nString
当然要讲讲字符串的简单操作了,插值可以将变量插入到字符串中,这个操作有点类似 Ruby 和 Shell 中的操作;字符串拼接操作,可以将两个字符串拼接在一起
1 2 3
name = "GinShio" "Hello #{name}" # "Hello GinShio" "Hello" <> " " <> name # "Hello GinShio"
基本操作
算术运算
Elixir 支持基本的 加 (+) 、 减 (-) 、 乘 (*) 、 除 (/),还有 div
和 mod
两个函数用于整数的除法和取模运算
|
|
逻辑运算
Elixir 支持逻辑运算,和其他语言差不多的 和(&&
) 、 或(||
) 、 非(!
)
|
|
当然还有三个操作符 and
、 or
和 not
,不过这些操作符的地一个参数必须是布尔类型
|
|
关系运算
常见的关系运算 ==
, !=
, <=
, <
, >=
和 >
,这与其他语言中的关系运算符相似
|
|
Elixir 还提供了两个关系运算符 ===
和 !==
,它们类似于 JS 中的严格比较
|
|
Elixir 中有一个很重要的特性,任意类型之间都可以比较,因为类型都有一个优先级,支持它们之间互相比较
|
|
集合
列表 (List)
列表是简单的集合,可以包含不同的数据类型,并且可以包含相同的值,内部使用 链表 实现,头插相较于尾插更快,获取长度也是 O(n) 的
头插使用 |
(cons) 进行操作,可以将元素插入到列表的头部,而拼接操作 ++
可以将两个列表拼接成一个,而减法操作 --
是基于严格比较的依照顺序的删除元素
|
|
列表可以选取 头 (head) 和 尾 (tail),头是列表的第一个元素,尾是除去第一个元素剩下的列表,也可以和 cons 结合起来获取列表的头部与尾部
|
|
元组 (Tuple)
元组与列表类似,不过元组使用的是连续内存实现,获取元组的长度很快,但修改很麻烦 (新的元组必须重新在内存中拷贝一份)
元组相较于列表没有那么多操作,元组更倾向于做一个不可变的数据类型,我们常常把二元组称为 pair,三元组称为 triple,而其他长度为n的元组称其为 N 元组 (n-tuple),这个概念在其他语言中也很常见
|
|
关键字列表 (Keyword List)
keywords 是一种特殊的列表,列表的元素是 pair,且 pair 的第一个元素必须是原子,其他行为与列表完全一致,不过 keywords 的语法可以不用写 pair 那么复杂,而是简便的
key: value
形式即可
keywords 有一些特殊的特性
- 键是 原子的
- 键是 有序的,即定义后顺序不会改变
- 键 不必唯一
|
|
映射 (Map)
映射也是键值对结构,与 keywords 类似,可以任意类型的数据为键,数据并不严格排序,但是键不能重复,重复的键会覆盖已有的键值对
map 的语法相对麻烦些,以 key => value
形式书写,好消息是如果键全是原子那么可以与 keywords 的语法类似。map可以像C++一样可以使用 operator[]
读取值,也可以使用
operator.
来读取值 (只可以用于读取原子键)
|
|
map 提供了 operator|
来更新一个键值对,但仅限于已存在的键值对,如果要添加一个新的键值对则需要用到 put 方法,当然 put 也可以用于更新
|
|
语句
小小的吐槽下,本身想把模式匹配放在控制语句之后,毕竟控制语句如果学过其他热门语言肯定是认识的,不过看到 case 时,它依赖模式匹配,好吧…那就先记模式匹配的笔记,好了,开始吧
模式匹配
模式匹配经常被用于函数、case等地方,用的还是蛮多的,且方便,模式匹配中必须穷尽示例用以匹配,如果默认值需要使用变量 _ 来接收默认情况,类似 C 语言的 switch 语句中的 default
匹配
我们一直没有讲 =
这个其他语言中的赋值符号,在 Erlang/Elixir 中这不止是赋值,准确的将,这是 匹配,接下来我们写一点 Erlang 的语句来体验一下匹配,Don't panic
,这和我们已经学会的 Elixir 几乎一样,如果要一直学习 Elixir 的话 Erlang 是逃不掉的,
Lisp 不知道能逃掉不
|
|
Erlang 中可以看到变量 Var 与 10 匹配,匹配之后便不能与 5 匹配了,与 5 匹配将出现错误,这与 Elixir 中是类似的
|
|
现在想想之前学习 list 时使用的 operator|
,取 head 和 tail 时其实也是匹配,匹配时对于不关注的变量可以使用变量 _ 替代
|
|
Pin
Elixir 在匹配时,匹配操作会同时做赋值操作,但 Erlang 中不会,我们可以使用 Pin
操作符 ^
来保持与 Erlang 中行为的一致
|
|
pin 也可以被用于常见的数据结构中
|
|
控制语句
控制语句主要分为3种
- if / unless*
- if 与 unless 是条件语句,与其他语言的 if 语句类似,if 与 unless 语义相反,在
Elixir 中都是宏定义
1 2 3 4 5 6 7 8 9 10 11 12
if String.valid?("Hello") do "Valid String" else "Invalid String" end # "Valid String" unless String.valid?("World") do "Invalid String" else "Valid String" end # "Valid String"
- case
- case 是一种匹配语句,基于模式匹配
1 2 3 4 5 6
case {:ok, "Hello World"} do {:ok, result} -> result {:error} -> "Uh oh!" _ -> "Catch all" end # Hello World
- cond
- 当我们需要匹配条件而不是值的时候,可以使用 cond,它的语法很像 case,按顺序匹配每一个条件,必须有一个为真的表达式,所以一般在结尾设置
true
匹配,有些像 Haskell 中的 守卫 表达式1 2 3 4 5 6
cond do 2 + 2 == 5 -> "2+2==5" 2 * 2 == 8 -> "2*2==8" true -> "All Error" end # "All Error"
- with
- with 类似于 case 语句,适用于嵌套的 case 语句,按照顺序一次匹配表达式,当失败时会返回对应的返回值
1 2 3 4 5 6 7 8 9 10 11
user = %{first: "Xin", last: "Liu"} with {:ok, first} <- Map.fetch(user, :first), {:ok, last} <- Map.fetch(user, :last) do last <> ", " <> first end # "Liu, Xin" with {:ok, first} <- Map.fetch(user, :first), {:ok, hello} <- Map.fetch(user, :hello) do hello <> ", " <> first end # :error
with 支持 else 语句,当 with 出现不匹配时,将其返回值在 else 中进行匹配, else 是类似 case 语法的模式匹配,需要穷尽匹配
1 2 3 4 5 6 7 8 9 10 11 12
with {:ok, number} <- Map.fetch(%{a: 1, b: 4}, :a), true <- is_even(number) do IO.puts "#{number} divided by 2 is #{div(number, 2)}" :even else :error -> IO.puts("We don't have this item in map") :error _ -> IO.puts("It's odd") :odd end
函数
匿名函数
先说说匿名函数吧,lambda 表达式是函数式编程语言的基础,lambda 演算 与 图灵机 堪称计算机程序设计语言的两大支柱,这里我们不学习那么深入,有兴趣嘛那就加油吧,我们简单说说 Elixir 中的 lambda,Elixir 中函数是一等公民,它们可以像变量一样使用与传递,威力十足!
简单的语法即 fn (params) -> statements
,这便会定义一个匿名函数,这个函数可以赋值给一个对象,或者传递进一个参数中;如果需要使用,则需要 name.(param)
来调用
|
|
很简单吧,这更像是一个完整的函数定义,还有一种做法是像 Shell 中使用函数参数,即使用参数的顺序来确定形式参数的使用;这就让 lambda 简单多了,当然也更难理解参数的含义了。我们在此简单的说明下, &()
这是一个匿名函数, &1
这是这个函数接收的第一个参数,以此类推
|
|
模式匹配可以用在函数中,我们先来看看匿名函数中的模式匹配,和 case 差不多,不过这是个函数
|
|
命名函数
命名函数一般被定义在模块中,使用关键字 def 定义,如果函数体与头在一行的使用可以使用 do:
来简单的书写
|
|
函数式语言往往也可以进行函数重载,不过他们一般只按照函数参数的个数进行重载,这在
Elixir 中也适用,在 Elixir 中函数的全程一般是 name/param_num
|
|
命名函数当然也可以很好的支持模式匹配,这样我们递归的实现会很简单
|
|
当然模式匹配与 Map 结合在一起,示例函数 hello/1 展示了只关注指定键的用法,当然我们也可以在关注指定键时接受整个 Map,即用模式匹配来接受
|
|
- Private (私有函数)
- 如果你不希望模块外调用某些函数,你可以使用
defp
来定义私有函数,这样定义的函数只能在模块内使用1 2 3 4 5 6 7 8 9 10
defmodule MyFibonacci do def fib(0), do: 0 def fib(1), do: 1 def fib(n), do: fib(1, 1, n) defp fib(ans, pre, 2), do: ans defp fib(ans, pre, n), do: fib(ans + pre, ans, n - 1) end MyFibonacci.fib(10) # 55 MyFibonacci.fib(100) # 354224848179261915075 # MyFibonacci.fib(1, 1, 5) # (UndefinedFunctionError) function MyFibonacci.fib/3 is undefined or private.
- Pipe
|>
(管道操作) - 没错你没听错,就是 pipe,有没有想起使用 *nix 时使用的管道,Elixir 中的 pipe 与
*nix 中的类似,都是将前一个调用的结果传递给后一个,这就很爽了,我们来对比一下,管道简直是嵌套调用的救星
1 2
foo(bar(baz(do_something()))) # Normal do_something() |> baz() |> bar() |> foo() # Pipe
那如果参数数量大于1怎么办,这些问题不大,带上参数就行,从 Pipe 来的参数优先入栈
1 2
"Hello, World" |> String.split() # ["Hello,", "World"] "Hello, World" |> String.split(", ") # ["Hello", "World"]
- Guard (守卫表达式)
- 可以被用于
函数
和case
当中,比方说我们现在有两个签名相同的函数hello/1
,我们需要通过 guard 来确定应该调用哪个函数1 2 3 4 5 6 7 8 9 10
defmodule MyModule do def hello(names) when is_list(names) do names |> Enum.join(", ") |> hello() end def hello(names) when is_binary(names) do "Hello, " <> names end end MyModule.hello("GinShio") # "Hello, GinShio" MyModule.hello(["GinShio", "iris"]) # "Hello, GinShio, iris"
我们试试在 case 中使用 guard,更多用法请查看 Guard clauses
1 2 3 4 5
case x do 1 -> :one 2 -> :two n when is_integer(n) and n > 2 -> :larger_than_two end
- Default (默认参数)
- 我们可以为函数设置一些默认值,使用语法
argument \\ value
1 2 3 4 5 6 7 8 9 10
defmodule MyModule do def hello(name, language_code \\ "en"), do: phrase(language_code) <> name defp phrase("en"), do: "Hello, " defp phrase("es"), do: "Hola, " defp phrase("zh"), do: "你好," end MyModule.hello("iris") # "Hello, iris" MyModule.hello("iris", "en") # "Hello, iris" MyModule.hello("iris", "es") # "Hola, iris" MyModule.hello("iris", "zh") # "你好,iris"
不过我们在默认参数与守卫表达式一起使用时,往往会出现一些问题,先来看看问题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
defmodule Greeter do def hello(names, language_code \\ "en") when is_list(names) do names |> Enum.join(", ") |> hello() end def hello(names, language_code \\ "en") when is_binary(names) do phrase(language_code) <> names end defp phrase("en"), do: "Hello, " defp phrase("es"), do: "Hola, " end # (CompileError): def hello/2 defines defaults multiple times. # Elixir allows defaults to be declared once per definition. Instead of: # def foo(:first_clause, b \\ :default) do ... end # def foo(:second_clause, b \\ :default) do ... end
有多个函数同时匹配时,默认参数这种模式很容易混淆,它不被 Elixir 喜欢,至于解决方法嘛还是有的,我们需要先声明这个函数,有点像 C++ 使用默认参数的方法
1 2 3 4 5 6 7 8 9 10 11
defmodule Greeter do def hello(names, language_code \\ "en") def hello(names, language_code) when is_list(names) do names |> Enum.join(", ") |> hello() end def hello(names, language_code) when is_binary(names) do phrase(language_code) <> names end defp phrase("en"), do: "Hello, " defp phrase("es"), do: "Hola, " end