在 function clauses 中, 我们运用 pattern matching, binding 值到函数的变量参数。pattern matching 也可帮助我们在 function clauses 中, 建立处理异常的 function。阅读完本文后, 我们将会发现 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!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据