构造函数、归约函数和转换函数
让我们了解 CRC 在 Phoenix 和 Elixir 模块中的作用。
流水线和函数组合在 Elixir 中扮演着重要角色。一种被称为 CRC 的模式在许多不同的 Elixir 模块中起到了关键作用。它的根源与通用函数 Enum.reduce/3 密切相关。让我们深入探讨一下。
在函数式语言中的 Web 框架普遍使用一种常见的模式变体。它们使用一种通用的数据类型来表示数据,并通过许多小型、专注的函数逐步对这些数据进行变换。例如,在 JavaScript 领域,Kent Dodds 提出的状态还原器模式就采用了类似的策略。Clojure 也有一个类似的框架,称为 Ring。
在 Phoenix 中,Plug 框架遵循相同的模式。让我们更详细地探索这一模式。
Plug 模式
在 Elixir 中,许多模块都关联着一个“核心”数据类型。String 模块处理字符串,Enum 模块处理可枚举类型,依此类推。经验丰富的 Elixir 开发者通常会努力让模块的公共函数与其核心类型相关联。构造函数从方便的输入创建核心类型的项;还原函数将核心类型的项转换为另一个同类型的项;转换函数则将核心类型转换为其他类型。我们将这种模式统称为 CRC。
到目前为止,CRC 可能看起来有些抽象,让我们通过一个简单而具体的例子来说明。
我们构建了一个包含这三种函数的模块。在下方终端中启动 IEx 来测试它:
1defmodule Number do 2 def new(string), do: Integer.parse(string) |> elem(0) 3 def add(number, addend), do: number + addend 4 def to_string(number), do: Integer.to_string(number) 5end
注意,这个小型模块处理整数,并包含三种类型的函数。它们都以整数作为输入参数、输出结果或两者兼有。new/1 函数是一个构造函数,用于从字符串输入创建模块类型的项。to_string/1 函数是一个转换函数,接收整数输入并生成另一种类型的结果,即字符串。add/2 还原函数则将整数作为输入和输出。
让我们用两种方式来使用它。首先,让我们在下方终端中使用 reduce/3 函数,将这三个函数组合起来:
1list = [1, 2, 3] 2total = Number.new("0") 3reducer = &Number.add(&2, &1) 4converter = &Number.to_string/1 5Enum.reduce(list, total, reducer) |> converter.()
我们应该看到类似这样的结果:
1"6"
现在,同样的函数不仅适用于还原器,也适用于管道操作,如下所示:
1[first, second, third] = list 2"0" |> Number.new 3|> Number.add(first) 4|> Number.add(second) 5|> Number.add(third) 6|> Number.to_string
在下方终端中测试这些函数,我们应该看到类似这样的结果:
1"6"
完美!Number 模块中的函数展示了 CRC 的一个示例,但这并非唯一实例。该模式非常适合将复杂任务(如处理复杂请求的响应)分解为多个小步骤。它还允许我们构建专注于单一功能的小型函数。
Phoenix 中的 CRC
Phoenix 使用 CRC 模式处理请求。许多 Phoenix 模块的核心类型是 Plug.Conn 模块定义的 connection 结构体。连接结构体代表一个 Web 请求。我们可以将响应分解为一系列小型还原函数,每个函数处理请求的一小部分,最后通过一个简短的转换函数完成最终输出。
程序的结构如下:
1connection 2|> process_part_of_request(...) 3|> process_part_of_request(...) 4|> render()
我们可以清晰地看到 CRC 的应用。Phoenix 本身充当构造函数,构建一个包含请求数据和响应数据的通用数据结构。最初,请求数据被填充了关于请求的信息,而响应数据为空。然后,Phoenix 开发者通过一系列小型还原函数逐步构建响应。最后,Phoenix 使用 render/1 转换函数将连接结构体转换为实际的响应。
让我们让这个例子更具体一些。假设我们希望 Web 服务器能够逐部分构建响应。我们可能会编写如下代码:
1connection = %{request_path: "http://mysite.com/"} 2reducer = fn acc, key, value -> Map.put(acc, key, value) end 3connection |> reducer.(:status, 200) |> reducer.(:body, :ok)
在下方 IEx 终端中运行此代码,我们应该看到如下结果:
1%{body: :ok, request_path: "http://mysite.com/", status: 200}
注意其中两个核心概念:首先是通用数据结构——连接;其次是接受一个参数(称为 acc,即累加器)以及两个其他参数的函数。我们的函数被称为“还原函数”,因为它可以将累加器和若干参数合并为一个新的累加器。
现在,借助这个虚构的程序,我们可以串联起一段描述 Web 请求的流程。对于我们的请求,我们先创建连接,然后将其传递给两个还原函数,分别设置状态为 200,正文为 :ok。当以这种方式构建完映射后,我们可以将其交还给 Web 服务器,通过传入 render/1 转换函数,将正确的状态和正文发送给客户端。
Plug.Conn 通用数据结构
Plug 是一个用于构建 Web 程序的框架,按函数逐步构建。插件(Plugs)可以是 Elixir 函数,也可以是支持一个名为 call 的小函数的微型模块。每个函数对连接(即 Plug.Conn 数据结构)进行微小的修改。Web 服务器只需让开发者轻松地将多个这样的插件串联起来,从而定义应用程序的各种策略和流程。
我们无需猜测其内部结构,可以直接查看。输入 iex -S mix 启动交互式 Elixir 环境,进入我们的 Phoenix 应用上下文。输入一个空的 %Plug.Conn{} 结构体并回车,我们应该看到默认值:
1%Plug.Conn{ 2 adapter: {Plug.MissingAdapter, :...}, 3 assigns: %{}, 4 body_params: %Plug.Conn.Unfetched{aspect: :body_params}, 5 cookies: %Plug.Conn.Unfetched{aspect: :cookies}, 6 halted: false, 7 host: "www.example.com", 8 method: "GET", 9 owner: nil, 10 params: %Plug.Conn.Unfetched{aspect: :params}, 11 path_info: [], 12 path_params: %{}, 13 port: 0, 14 private: %{}, 15 query_params: %Plug.Conn.Unfetched{aspect: :query_params}, 16 query_string: "", 17 remote_ip: nil, 18 req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, 19 req_headers: [], 20 request_path: "", 21 resp_body: nil, 22 resp_cookies: %{}, 23 resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}], 24 scheme: :http, 25 script_name: [], 26 secret_key_base: nil, 27 state: :unset, 28 status: nil 29}
响应状态是标准化的 HTTP 状态码。
这就是“通用数据结构”部分。接下来,我们来看还原函数。
Plug 中的还原函数
现在我们已经了解了 Plug.Conn,它是连接 Phoenix 程序的核心数据。我们不需要掌握太多细节,就能理解大多数 Phoenix 应用文件,只要掌握三个核心概念即可:
- 插件是还原函数。
- 它们以
Plug.Conn结构体作为第一个参数。 - 它们返回一个
Plug.Conn结构体。
当我们看到 Phoenix 配置代码时,常常会看到大量插件。当看到插件列表时,可以想象它们之间用管道操作符连接。例如,当我们看到如下代码:
1plug Plug.MethodOverride 2plug Plug.Head 3plug Plug.Session, @session_options 4plug PentoWeb.Router
我们可以将其在脑海中转化为:
1connection 2|> Plug.MethodOverride.call() 3|> Plug.Head.call() 4|> Plug.Session.call(@session_options) 5|> PentoWeb.Router.call()
有了这些背景知识,我们将深入探究 Phoenix 架构的核心。即使你对 Phoenix 的经验不多,也能理解其本质。请记住,这些信息将在后续理解 LiveView 运行机制时非常有用。