在 function clauses 中, 我们运用 pattern matching, binding 值到函数的变量参数。pattern matching 也可帮助我们在 function clauses 中, 建立处理异常的 function。阅读完本文后, 我们将会发现 pattern matching 会使得我们的代码更加的容易阅读、维护与扩展。
elixir function clauses pattern matching
在 “云端分布式架构下的编程语言: ELIXIR; function clauses” 一文中, 我们使用 function clauses, pattern matching 使得 path: “/wildthings”, “/bears”, 都会获得信息的转换。
def route(conv, "GET", "/wildthings") do
%{ conv | resp_body: "Bears, Lions, Tigers"}
end
def route(conv, "GET", "/bears") do
%{ conv | resp_body: "Teddy, Smokey, Paddington"}
end
我们现新增一 request; path: “/bigfoot”
request = """
GET /bigfoot HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
这新增的 request , 在 function clauses 中, 便找不到相匹配 (match) 的 function, 来进行信息的转换。所以, 会获得如下的错误信息:
%{method: "GET", path: "/bigfoot", resp_body: "", status: nil}
** (FunctionClauseError) no function clause matching in Servy.Handler.route/3
我们将在原 function clauses 中, 加入如下的 function; 处理当 path 既不是 “/wildthings”, 也不是 “/bears” 时的信息的转换。
def route(conv, _method, path) do
%{ conv | resp_body: "No #{path} here!"}
end
为何 function route(conv, _method, path), 能够处理当 path 既不是 “/wildthings”, 也不是 “/bears” 时的信息的转换 ?
我们需先来看看整个的 function clauses:
def route(conv) do
route(conv, conv.method, conv.path)
end
def route(conv, "GET", "/wildthings") do
%{ conv | resp_body: "Bears, Lions, Tigers"}
end
def route(conv, "GET", "/bears") do
%{ conv | resp_body: "Teddy, Smokey, Paddington"}
end
def route(conv, _method, path) do
%{ conv | resp_body: "No #{path} here!"}
end
elixir 会使用 pattern matching 对 function clauses 中的 function 的 “参数”, 进行 :
- 是否相匹配 (matching) 的校验
- 变量的值的 binding
当 elixir 在 function clauses 中, 能找到 “参数” 相匹配 (matching) 的 function 时, 便由此 function 执行相对应的信息转换。否则, elixir 将会继续在 function clauses 中, 寻找下一个 “参数” 相匹配 (matching) 的 function 。

path: “/wildthings”, “/bears” 都可以在 function clauses 中找到能相匹配 (matching) 的 function。
而当 path 既不是 “/wildthings”, 也不是 “/bears” 时, pattern matching 便会对 function route(conv, _method, path) 进行变量参数 ;path; 的 binding。 所以, path 将会是除了 “/wildthings”, “/bears” 以外的任何路径的值。而另一方面, 因为, 变量参数 ;method; 将不需要做 pattern matching, 所以, 以 _mathod 的方式来表示。
path: “/bigfoot”, 将会由 function route(conv, _method, path) 進行信息的转换; 变量参数 ;path; 将会 binding 为 “/bigfoot”。
def route(conv, _method, path) do
%{ conv | resp_body: "No #{path} here!"}
end
%{method: "GET", path: "/bigfoot", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 17
No /bigfoot here!
当 path: “bigfoot” 时, 照理说, http 的 status 应该是 404, “Not Found”, 但, 我们所回应的 http 的 status 却还是 200, “OK”。
我们接下来要来解决 http status 的问题。
http status 是由 http code 与 http description 所组成。所以, 我们便利用 map 的结构来体现 http status。
- map 的 key 是 http code
- map 的 value 是 http description。
我们更进一步的利用 private function (defp) , 将这个 http status 的 map, 只能在 module 内被取得。
defp status_reason(code) do
%{
200 => "OK",
201 => "Created",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error"
}[code]
end
因为, map 的 key 是值 (整数), 而不是 atom, 所以, map 的 key 与 value 之间是 =>, 而不是 :。
map 中的 [code] 代表著, 我们将以 private function status_reason 的参数; code; 作为 map 的 key; http code; 而找到相对应的 value; http description。
例如: status_reason(200)
- [code] = [200]
- map 的 key = 200
- map 的 value = “OK”
定义好了 http status 的 map 结构, 我们还需在 function parse 所会传回的 map 结构中, 加入 http code; status。
def parse(request) do
# TODO: Parse the request string into a map:
[method, path, _] =
request
|> String.split("\n")
|> List.first
|> String.split(" ")
%{ method: method,
path: path,
resp_body: "",
status: nil # http code
}
end
我们现在可以在 function clauses 中, 加入各 function 所对应的 http code; status。例如: function route(conv, “GET”, “/wildthings”), 将 http code; status; 设为 200。
def route(conv, "GET", "/wildthings") do
%{ conv | status: 200, resp_body: "Bears, Lions, Tigers"}
end
def route(conv, "GET", "/bears") do
%{ conv | status: 200, resp_body: "Teddy, Smokey, Paddington"}
end
def route(conv, _method, path) do
%{ conv | status: 404, resp_body: "No #{path} here!"}
end
在 function format_response(conv) 中:
#{conv.status}: 输出 http code。
#{status_reason(conv.status)}: 以 http code 为 key, 在 function status_reason 中找到相对应的 http description, 并将 http description 输出。
def format_response(conv) do
# TODO: Use values in the map to create an HTTP response string:
"""
HTTP/1.1 #{conv.status} #{status_reason(conv.status)}
Content-Type: text/html
Content-Length: #{String.length(conv.resp_body)}
#{conv.resp_body}
"""
end
现在, 输出的 response 字串, 就会包含 http code 与 http description。例如: 200 OK, 404 Not Found。
%{method: "GET", path: "/bears", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 25
%{method: "GET", path: "/bigfoot", resp_body: "", status: nil}
HTTP/1.1 404 Not Found
Content-Type: text/html
Content-Length: 17
接下来, 我们将再加入另一个 request; 在 path 字串 “bears” 后, 再嵌入字串 bear id。
request = """
GET /bears/1 HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
在 elixir, 字串的嵌入是使用: <>
iex(62)> "/bears/" <> "1"
"/bears/1"
我们也可使用 pattern matching 的方式; binding 某个字串到变量字串 bear id。
iex(63)> "bears/" <> id = "bears/1"
"bears/1"
iex(64)> id
"1"
所以, <>, pattern matching, 让我们可以在字串 “bears” 之后, 嵌入任意的字串 bear id。
现在, 我们知道该如何的运用 <>, pattern matching 在 function clauses 中, 新增加一 function, 来处理新增的 request; path: “/bears/1″。
def route(conv, "GET", "/bears" <> id) do
%{ conv | status: 200, resp_body: "Bear #{id}"}
end
函数的参数; “/bears” <> id; 会与 “bears/1” 產生 pattern matching。所以, 当 request 的 path 是 “bears/1” 时, patten matching 便会将字串 “1”, binding 到变量参数: “id”。
样例代码如下:
defmodule Servy.Handler do
def handle(request) do
request
|> parse
|> log
|> route
|> format_response
end
def log(conv), do: IO.inspect conv
def parse(request) do
# TODO: Parse the request string into a map:
[method, path, _] =
request
|> String.split("\n")
|> List.first
|> String.split(" ")
%{ method: method,
path: path,
resp_body: "",
status: nil # http code
}
end
def route(conv) do
route(conv, conv.method, conv.path)
end
def route(conv, "GET", "/wildthings") do
%{ conv | status: 200, resp_body: "Bears, Lions, Tigers"}
end
def route(conv, "GET", "/bears") do
%{ conv | status: 200, resp_body: "Teddy, Smokey, Paddington"}
end
def route(conv, "GET", "/bears" <> id) do
%{ conv | status: 200, resp_body: "Bear #{id}"}
end
def route(conv, _method, path) do
%{ conv | status: 404, resp_body: "No #{path} here!"}
end
def format_response(conv) do
# TODO: Use values in the map to create an HTTP response string:
"""
HTTP/1.1 #{conv.status} #{status_reason(conv.status)}
Content-Type: text/html
Content-Length: #{String.length(conv.resp_body)}
#{conv.resp_body}
"""
end
defp status_reason(code) do
%{
200 => "OK",
201 => "Created",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error"
}[code]
end
end
request = """
GET /wildthings HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
request = """
GET /bears HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
request = """
GET /bears/1 HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
request = """
GET /bigfoot HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
执行结果:
%{method: "GET", path: "/wildthings", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Bears, Lions, Tigers
%{method: "GET", path: "/bears", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 25
Teddy, Smokey, Paddington
%{method: "GET", path: "/bears/1", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 7
Bear /1
%{method: "GET", path: "/bigfoot", resp_body: "", status: nil}
HTTP/1.1 404 Not Found
Content-Type: text/html
Content-Length: 17
No /bigfoot here!