
在本文中, 我们将探讨 pattern matching 是如何的使得 elixir 的代码能更加容易的被理解、维护、扩展与裁减的。
elixir pattern matching
我们先來看看 pattern matching 的一些的用法:
map conv 有 key: method, key: path:
iex(3)> conv = %{ method: "GET", path: "/wildlife"}
- 当 map 的 key: path 的 value 是 “/wildlife”, 即使 map 只有 key: path , 而沒有 key: method, map 與 map conv, 还是 pattern matching 的。
iex(4)> %{path: "/wildlife"} = conv
%{method: "GET", path: "/wildlife"}
- 当 map 的 key: path 的 value 不是 “/wildlife” 时, map 与 map conv 就不是 pattern matching。如以下的例子: map 的 key: path 的 value 是 “/bears”。
iex(5)> %{path: "/bears"} = conv
** (MatchError) no match of right hand side value: %{method: "GET", path: "/wildlife"}
- 当 map 的 key 与 map conv 的 key 不匹配时, map 与 map conv 就不是 pattern matching。如以下的例子: 因为, map 的 key: name, 只存在于 map, 而不存在于 map conv。所以, map 的 key 与 map conv 的 key 不匹配, map 与 map conv 就不是 pattern matching。
iex(5)> %{name: "ken", path: "/wildfile"} = conv
** (MatchError) no match of right hand side value: %{method: "GET", path: "/wildlife"}
- 我们也可将 map conv bind 到 map:
iex(7)> %{method: method, path: "/wildlife"} = conv
%{method: "GET", path: "/wildlife"}
iex(8)> method
"GET"
iex(9)> %{method: method, path: path} = conv
%{method: "GET", path: "/wildlife"}
iex(10)> path
"/wildlife"
iex(11)> method
"GET"
交流完了 pattern matching, 我们将再加入一 request 信息; path: “/wildlife”。
request = """
GET /wildlife HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
我们将使用上述 pattern matching 的用法, 将新加入的 request 信息的 path: “/wildlife” 改成 path: “/wildthings”。
首先, 我们将在 pipelines 中加入新的 function; rewrite_path。function rewrite_path 负责将 path: “/wildlife” 改成 path: “/wildthings”。
def handle(request) do
request
|> parse
|> rewrite_path
|> log
|> route
|> format_response
end
function rewrite_path 负责将 path: “/wildlife” 改成 path: “/wildthings” 。所以, function rewrite_path 只需使用 path: “/wildlife” 与 conv 来进行 pattern matching。
def rewrite_path(%{path: "/wildlife"} = conv) do
%{ conv | path: "/wildthings" }
end
执行的结果:
kenchfang$ elixir lib/servy/handler.ex
** (FunctionClauseError) no function clause matching in Servy.Handler.rewrite_path/1
执行的结果出现了错误; 因为, 目前, function rewrite_path 只能处理 path: “/wildlife”。所以, 其他的 path, 如: path: “/bears”, 就找不到 pattern matching 的 function 。
当其他的 path, 如: path: “/bears”, 找不到 pattern matching 的 function 时, 将会使得在 function handle 所建立的 pipelines, 在 function rewrite_path 处断开。所以, elixir 就报错了。
为了修正这个错误, 我们就再新增个 function rewrite_path; 当 path 不是 “/wildlife” 时, 就将 map conv 直接的回传到 function handle 所建立的 pipelines 上。
def rewrite_path(conv), do: conv
样例代码如下:(主要是新增了由两个 function rewrite_path 所构成的 function clauses)。
defmodule Servy.Handler do
def handle(request) do
request
|> parse
|> rewrite_path
|> log
|> route
|> format_response
end
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 rewrite_path(%{path: "/wildlife"} = conv) do
%{ conv | path: "/wildthings" }
end
def rewrite_path(conv), do: conv
def log(conv), do: IO.inspect conv
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
request = """
GET /wildlife HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
现在执行的结果, 就如我们所预期的:
kenchfang$ elixir lib/servy/handler.ex
%{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!
%{method: "GET", path: "/wildthings", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Bears, Lions, Tigers
接下来我们将利用本文所交流的 pattern matching 的方法, 改写先前的 function clauses route; 回顾一下: 如下是先前的 function clauses route 的写法。
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
精简后的 function clauses route 如下:
- 将原先需 3 个参数; conv, conv.method, conv.path; 的 function clauses route, 精简成只需 1 个参数; conv。
- 删去 function route(conv)。
def route(%{method: "GET", path: "/wildthings"} = conv) do
%{ conv | status: 200, resp_body: "Bears, Lions, Tigers"}
end
def route(%{method: "GET", path: "/bears"} = conv) do
%{ conv | status: 200, resp_body: "Teddy, Smokey, Paddington"}
end
def route(%{method: "GET", path: "/bears/" <> id} = conv) do
%{ conv | status: 200, resp_body: "Bear #{id}"}
end
def route(%{path: path} = conv) do
%{ conv | status: 404, resp_body: "No #{path} here!"}
end
当 function clauses route 精简到只需 1 个参数; conv; 时, 我们就将整个文件代码的结构, 精简到只有 pipelines; 在 pipelines 上, 对各个的 function clauses; 如: rewrite_path, route, track; 能直接的进行 pattern matching。
- 在 pipelines 上, 能直接的就对各个的 function clauses, 进行 pattern matching 时, 我们将能完全的避免掉 “Deeply Nested Logic”, 而能大幅的提升代码的可读性与可维护性。
- 在 pipelines 上, 能直接的就对各个的 function clauses, 进行 pattern matching 时, 我们就能很容易的进行 “扩展”; 将所需新增的 function, 直接的就加到适当的 pipelines 或 function clauses 中。
- 在 pipelines 上, 能直接的就对各个的 function clauses, 进行 pattern matching 时, 我们就能很容易的进行 “裁減”; 将已不再需要的 function, 直接的从 pipelines 或 function clauses 中移除。
当整个文件代码的结构, 精简到只有 pipelines 時, 我們將能真正的保障: 代码的可读性、可维护性、可扩展性与可裁减性。
样例代码如下:
defmodule Servy.Handler do
def handle(request) do
request
|> parse
|> rewrite_path
|> log
|> route
|> track
|> format_response
end
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 rewrite_path(%{path: "/wildlife"} = conv) do
%{ conv | path: "/wildthings" }
end
def rewrite_path(conv), do: conv
def log(conv), do: IO.inspect conv
def route(%{method: "GET", path: "/wildthings"} = conv) do
%{ conv | status: 200, resp_body: "Bears, Lions, Tigers"}
end
def route(%{method: "GET", path: "/bears"} = conv) do
%{ conv | status: 200, resp_body: "Teddy, Smokey, Paddington"}
end
def route(%{method: "GET", path: "/bears/" <> id} = conv) do
%{ conv | status: 200, resp_body: "Bear #{id}"}
end
def route(%{path: path} = conv) do
%{ conv | status: 404, resp_body: "No #{path} here!"}
end
def track(%{status: 404, path: path} = conv) do
IO.puts "Warning: #{path} is on the loose!"
conv
end
def track(conv), do: conv
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
request = """
GET /wildlife HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
执行的结果:
kenchfang$ elixir lib/servy/handler.ex
%{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: 6
Bear 1
%{method: "GET", path: "/bigfoot", resp_body: "", status: nil}
Warning: /bigfoot is on the loose!
HTTP/1.1 404 Not Found
Content-Type: text/html
Content-Length: 17
No /bigfoot here!
%{method: "GET", path: "/wildthings", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Bears, Lions, Tigers