Elixir 简明笔记(十二) --- 模块
模块
与大多数语言一样,Elixir同样也提供了模块的功能。模块能够更好的组织代码。前面我们见识了匿名函数,作为函数式语言,肯定会有命名函数。不同于别的语言,elixir的命名函数必须定义在模块之中。
模块是一些命名函数的集合,类似命名空间,把不同功能的函数组织在不同的模块里面。模块可以自己定义,也可以通过安装第三方包。当然Elixir提供了一下非常有用的模块,这些称之为标准库。通常这些模块属于内核。例如IO
模块,使用模块名.函数名(参数) ModuleName.function_name(args)
调用模块的函数:
iex(1)> IO.puts "Hello World" # 调用 IO 模块的 puts 方法,用于向终端打印字符串
Hello World
:ok # puts 函数的返回,作为函数式语言,函数通常都有返回值
模块定义
定义模块很简单,使用defmodule
宏定义即可,对于defmodule可以把其暂时理解为其他语言终端关键字
。在模块里面,可以使用 def
结构定义函数。新建一个文件夹learn-elixir
作为我们应用的根目录,然后编辑文件geometry.ex
,例如:
defmodule Geometry do # 定义模块
def rectangle_area(a, b) do
a * b
end
end
通常模块使用 CamelCase 命名法。即单词的首字母都是大写。
模块执行
geometry.ex 文件定义了Geometry模块,Geometry模块定义了rectangle_area方法。下面可以执行模块的函数。可以使用elixir 来执行 geometry.ex。运行 elixir geometry.ex
。并没有反馈。程序被执行了,可是代码仅定义了一个模块,没有输出。因此使用 iex 来执行。
learn-elixir iex geometry.ex
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.0.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Ge
GenEvent GenServer Geometry
iex(1)> Geometry.rectangle_area 2, 5
10
iex(2)> ls
geometry.ex
:ok
使用 iex 执行ex文件,会把模块上下文导入到iex中,因此可以直接运行Geometry的方法。如果直接运行iex,想要得到模块,必须先编译。编译ex也有两种方式,一种是使用elixirc
命令
☁ learn-elixir elixirc geometry.ex
☁ learn-elixir ls
Elixir.Geometry.beam geometry.ex
当然目录会生成 .beam 字节码,该文件是运行在Erlang虚拟机上的文件。从Elixir.Geometry.beam 文件也可以看出,所有Elixir模块,其实是挂载在Elixir
这个模块。如果使用iex,也可以编译,先删掉Elixir.Geometry.beam 。然后启动iex:
☁ learn-elixir ls
Elixir.Geometry.beam geometry.ex
☁ learn-elixir rm -rf Elixir.Geometry.beam
☁ learn-elixir ls
geometry.ex
☁ learn-elixir iex
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.0.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> c "geometry.ex"
[Geometry]
iex(2)> ls
Elixir.Geometry.beam geometry.ex
:ok
iex(3)> Geometry.rectangle_area 1 2
2
只要是编译后,在编译后的文件目录下启动iex,模块都能导入iex的上下雯。
模块编译
我们知道了iex导入模块运行。通常情况下,执行程序不会使用 iex 这种交互式的平台。因此我们定义一个程序执行的入口文件,并在入口文件中应用执行模块函数。新建一个main.ex 文件。
IO.puts "start main"
ret = Geometry.rectangle_area 1, 2
IO.puts "#{ret}"
IO.puts "end main"
然后运行 main.ex可以看到模块被执行了。
☁ learn-elixir elixir main.ex
start main
2
end main
☁ learn-elixir
模块也可以被导入文件中,使用 import 命令,就能将模块中的函数全部导入当前的执行环境中。修改main.ex 如下:
IO.puts "start main"
import Geometry
ret = rectangle_area 1, 2
IO.puts "#{ret}"
IO.puts "end main"
再次运行同样可以得到修改前的结果。须知,之所以可以导入模块,原因是有了编译的.beam文件,如果干掉这个文件,再执行将会报错:
☁ learn-elixir rm -rf Elixir.Geometry.beam
☁ learn-elixir elixir main.ex
** (CompileError) main.ex:3: module Geometry is not loaded and could not be found
(stdlib) lists.erl:1352: :lists.mapfoldl/3
(stdlib) lists.erl:1353: :lists.mapfoldl/3
模块嵌套
elixir的模块也是可以嵌套。并且还提供了一个.
的语法糖。修改 geometry.ex 并编译:
defmodule Geometry do
defmodule Rectangle do
def area(a, b) do
a * b
end
end
def whoIam? do
IO.puts __MODULE__
end
end
编译之后,我们看见了如下四个文件:
☁ learn-elixir ls
Elixir.Geometry.Rectangle.beam geometry.ex
Elixir.Geometry.beam main.ex
可以看到,elixir 编译一共产生了两个模块,一个是Geometry模块,另外一个是Geometry.Rectangle模块。内嵌定义的模块,编译之后,会把全路径给写出来。也就是外套的模块也会被加入。再修改 main.ex ,尝试使用这两个模块。
import Geometry
# 调用 Geometry 的 whoIam?方法
whoIam?
ret = Rectangle.area 1, 2
IO.puts "#{ret}"
运行main之后,输出
☁ learn-elixir elixir main.ex
Elixir.Geometry
** (UndefinedFunctionError) undefined function: Rectangle.area/2 (module Rectangle is not available)
Rectangle.area(1, 2)
main.ex:6: (file)
(elixir) lib/code.ex:307: Code.require_file/2
出错了,看起来是Rectangle这个模块不存在,前面我们把Rectangle嵌套了再Geometry模块中,为什么导入Geometry之后不能接着使用 Rectangle?再次修改main.ex。
import Geometry
import Geometry.Rectangle
whoIam?
ret = area 1, 2
IO.puts "#{ret}"
此时可以得到正确的结果。由此可见,所谓的嵌套结果,并没有命名空间的概念。对于elixir,模块的嵌套只是代码组织的一种手段,再编译后。只存在 Geometry 模块和 Geometry.Rectangle 模块,并不存在 Rectangle 模块。这一点从编译后的文件可以看得出来。因此,为了嵌套模块,elixir提供了.
这样的操作符。上述的内嵌代码可以修改如下:
defmodule Geometry do
def whoIam? do
IO.puts __MODULE__
end
end
defmodule Geometry.Rectangle do
def area(a, b) do
a * b
end
end
使用点.
来代替嵌套,编译再运行main.ex。同样得到了正确的结果。再一次强调,无论是.
还是嵌套模块,他们只是用来组织代码,一旦编译后,Geometry 和 Geometry.Rectangle是两个完全不同的模块,并且他们之间没有任何联系。
基于上面对模块的认识,既然嵌套模块再编译后都仅和模块名有关,因此可以把不同的模块放到一个文件夹,通过文件夹来组织模块。
新建文件夹,geometry 和 rectangle ,目录结构如下:
├── geometry
│ ├── geometry.ex
│ └── rectangle
│ └── rectangle.ex
└── main.ex
然后修改 geometry/geometry.ex 如下:
defmodule Geometry do
def whoIam? do
IO.puts __MODULE__
end
end
geometry/rectangle/rectangle.ex
defmodule Geometry.Rectangle do
def area(a, b) do
a * b
end
end
然后分别编译这两个文件结果如下:
☁ learn-elixir ls
geometry main.ex
☁ learn-elixir elixirc geometry/geometry.ex
☁ learn-elixir ls
Elixir.Geometry.beam geometry main.ex
☁ learn-elixir elixirc geometry/rectangle/rectangle.ex
☁ learn-elixir ls
Elixir.Geometry.Rectangle.beam geometry
Elixir.Geometry.beam main.ex
☁ learn-elixir elixir main.ex
Elixir.Geometry
2
尽管两个模块文件都被文件夹组织在一起了,但是最后编译后的文件,还是再编译的当前目录下。由此可见,在模块中编写代码的时候,模块间相互引用,也变得很方便,因为都是相对根目录而言的导入。
再次修改 geometry.ex ,我们需要测试再 geometry.ex 使用 Geometry.Rectangle模块。
defmodule Geometry do
def whoIam? do
IO.puts __MODULE__
end
import Geometry.Rectangle
def call_rectangle_area(a, b) do
area(a, b)
end
end
然后修改 main.ex
import Geometry
ret = call_rectangle_area(1, 2)
IO.puts "#{ret}"
因为修改了文件,需要对 geometry.ex 重新编译,编译之后运行 main.ex
☁ learn-elixir elixirc geometry/geometry.ex
geometry/geometry.ex:2: warning: redefining module Geometry
☁ learn-elixir elixir main.ex
2
总结
综上所述,对于elixir模块的探索大致可以归结为模块名作为模块的区分,.
或者嵌套模块只是代码组织的一种形式,在编译后,并没有相关的联系。这从编译后的.beam
文件可以看出来。
在不同模块中调用其他模块的函数,需要先把模块导入。当然关于导入模块,elixir提供了 import,require, use, 以及 alias等方式。它们到底有什么差别呢?下一篇笔记再来深究。