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等方式。它们到底有什么差别呢?下一篇笔记再来深究。

你可能感兴趣的:(Elixir 简明笔记(十二) --- 模块)