Pattern matching 在 elixir 中广泛的被应用。使得 elixir 的代码易读、易维护、易扩展、易裁减。

本文中将持续的探讨 pattern matching 在 elixir 中的另一个的应用; 读取文件。

我们将新增如下的 function clauses route 来读取文件:

def route(%{method: "GET", path: "/about"} = conv) do

end

对应到新增的 function clauses route , 我们新增了如下的 request 信息:

request = """
GET /about HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*

"""

先将新增的 function clauses route 与 request 信息放一边。

我们先来看看 elixir 读取文件的 function; File.read。

File.read 会回传 tuples。

当文件的读取是正常的完成时, 回传的 tuples 便会包含:

  • 表示读取是正常完成的 atom; :ok。
  • 所读取的文件的内容; binary。
 def read(path)  
Returns {:ok, binary}, where binary is a binary data object that contains the contents of path

当文件的读取是发生异常时, 回传的 tuples 便会包含:

  • 表示读取是异常的 atom; :error。
  • 异常的原因; 以 atom 来表示。例如: :enoent 表示, “文件不存在”。
def read(path) 
Returns {:error, reason}
Typical error reasons:
  • :enoent  - the file does not exist
  • :eacces  - missing permission for reading the file, or for searching one of the parent directories
  • :eisdir  - the named file is a directory
  • :enotdir - a component of the file name is not a directory; on some platforms, :enoent is returned instead
  • :enomem  - there is not enough memory for the contents of the file

交流完了 File.read, 我们将在 elixir 的根目录下, 创建 pages 目录, 并在 pages 目录内, 创建我们所要读取的文件; about.html。

<h1>Clark's Wildthings Refuge</h1>

<blockquote>
    When we contemplate the whole globe as one great dewdrop,
    striped and dotted with continents and islands, flying through
    space with other stars all singing and shining together as one,
    the whole universe appears as an infinite storm of beauty.
    -- John Muir
</blockquote>

我们的目录结构如下图:

当 elixir 执行 handler.ex 时, 我们必需要告诉 elixir 如何能从文件 handler.ex 所在的绝对位置找到文件 about.html ?

观察目录的结构; 文件 handler.ex 经由路径 ../../pages; 目录阶层: ../../, 再加上 pages 的目录; 就能找到文件 about.html 所在的目录 (位置):

../../pages 便称作是文件 handler.ex 与文件 about.html 间的相对路径。

  • elixir 的 Path.expand 便会从文件 handler.ex 所在的绝对位置; __DIR__; 开始, 根据 ../../pages, 找到文件 about.html 所在的目录 (位置)。
page_path = Path.expand("../../pages", __DIR__)
  • elixir 的 Path.join 会找到在 page_path ; ../../pages; 内的文件; about.html。
file = Path.join(page_path, "about.html")

总结一下: 这两段代码将能使 elixir 从文件 handler.ex 所在的绝对位置找到文件 about.html:

page_path = Path.expand("../../pages", __DIR__)
file = Path.join(page_path, "about.html")

毫无疑问的, 我们可以使用 pipelines, 使得代码更加的简洁、易懂。

file =
      Path.expand("../../pages", __DIR__)
      |> Path.join("about.html")

elixir 现在已能从文件 handler.ex 所在的绝对位置, 找到文件 about.html。所以, 我们现在可以使用 File.read, 去读取 about.html; 我们先使用 case 来处理文件读取的正常与异常的场景:

case File.read(file) do
        {:ok, content} ->
           %{ conv | status: 200, resp_body: content}

        {:error, :enoent} ->
           %{ conv | status: 404, resp_body: "File not found"}

        {:error, reason} ->
           %{ conv | status: 500, resp_body: "File error #{reason}"}
    end

File.read(file) 会读取 about.html, 并回传如下的 tuples:

  • {:ok, content}
    • :ok, 读取 about.html 成功。
    • content 是 about.html 的内容。
  • {:error, :enoent}
    • :error, 读取 about.html 失败。
    • :enoent, about.html 不存在。
  • {:error, reason}
    • :error, 读取 about.html 失败。
    • reason; 读取 about.html 失败的原因, 但不包含 about.html 不存在。

elixir 便会根据 File.read(file), 所回传的 tuples, 在 case 中进行 pattern matching; 根据文件读取的正常与异常的场景, rebind map conv。

新增的 function clauses route 的样例代码如下:

def route(%{method: "GET", path: "/about"} = conv) do
    file =
      Path.expand("../../pages", __DIR__)
      |> Path.join("about.html")

    case File.read(file) do
        {:ok, content} ->
           %{ conv | status: 200, resp_body: content}

        {:error, :enoent} ->
           %{ conv | status: 404, resp_body: "File not found"}

        {:error, reason} ->
           %{ conv | status: 500, resp_body: "File error #{reason}"}
    end
end

执行的结果如下:

%{method: "GET", path: "/about", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 339

<h1>Clark's Wildthings Refuge</h1>

<blockquote>
    When we contemplate the whole globe as one great dewdrop,
    striped and dotted with continents and islands, flying   through space with other stars all singing and shining together as one, the whole universe appears as an infinite storm of beauty.
    -- John Muir
</blockquote>

elixir 除了上述的 case 的写法外, 我们也可运用 pattern matching 与 function clauses 的方式; 在 pipelines 上进行 tuples 的 pattern matching。

我们改写 function clauses route 的 pipelines; 在 pipelines 上新增了 function clauses handle_file。

  • 在 pipelines 上根据 File.read 所传回的 tuples, matching function clauses handle_file, 使得 File.read 在读取 about.html 时的正常或异常的场景, 都会 matching 到相对应的 function clauses handle_file, 而能对 map conv 进行适当的 rebinding。
def route(%{method: "GET", path: "/about"} = conv) do
     Path.expand("../../pages", __DIR__)
     |> Path.join("about.html")
     |> File.read
     |> handle_file(conv)
end

function clauses handle_file 的样例代码如下:

def handle_file({:ok, content}, conv) do
    %{ conv | status: 200, resp_body: content}
end

def handle_file({:error, :enoent}, conv) do
    %{ conv | status: 404, resp_body: "File not found"}
end

def handle_file({:error, reason}, conv) do
    %{ conv | status: 500, resp_body: "File error #{reason}"}
end

完整的样例代码如下:

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(%{method: "GET", path: "/about"} = conv) do
     Path.expand("../../pages", __DIR__)
     |> Path.join("about.html")
     |> File.read
     |> handle_file(conv)    
  end

  def handle_file({:ok, content}, conv) do
    %{ conv | status: 200, resp_body: content}
  end

  def handle_file({:error, :enoent}, conv) do
    %{ conv | status: 404, resp_body: "File not found"}
  end

  def handle_file({:error, reason}, conv) do
    %{ conv | status: 500, resp_body: "File error #{reason}"}
  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

执行的结果是一样的; 但代码比使用 case, 更易读、更易维护、更易扩展、更易裁减。

%{method: "GET", path: "/about", resp_body: "", status: nil}
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 339

<h1>Clark's Wildthings Refuge</h1>

<blockquote>
    When we contemplate the whole globe as one great dewdrop,
    striped and dotted with continents and islands, flying through
    space with other stars all singing and shining together as one,
    the whole universe appears as an infinite storm of beauty.
    -- John Muir
</blockquote>

发表评论

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

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