云端分布式架构下的编程语言: elixir pattern matching

在本文中, 我们将探讨 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

发表评论

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

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

滚动至顶部