(译)Elixir Tip: Case vs. With

2023-10-21 10:17:53 浏览数 (3)

从 1.2 版本开始, with 运算符是需要点时间去理解的 ELixir 特性之一. 它经常在使用 case 的情形下使用, 反之亦然. 两者的不同在于如果没有可以匹配到的子句, with 将失败, 而 case 将抛出一个不匹配 (no-match) 的错误 (CaseClauseError).

是不是有一点点疑惑, 让我们从最基本的使用开始看看.

使用 case 进行精准匹配, 你非常确定至少有一个是可以被匹配到的:

代码语言:javascript复制
case foo() do
  cond1 -> expression1
  cond2 -> expression2
  cond3 -> expression3
  _ -> default_expression
end

一个常见的 case 使用情景是对潜在的错误进行模式匹配:

代码语言:javascript复制
case foo() do
  {:ok, res} -> do_something_with_result(res)
  {:error, err} -> handle_error(err)
end

目前为止, 一切都看起来很好. 让我们来看一个常见的日常工作场景案例. 试想一下, 我们有一个这样的操作 (它可以是外部的 API 调用、IO 或者数据库操作), 我们想执行第二个这样的操作, 但只有当第一个操作成功的时候才执行. 这个行为我们怎么实现?

回想一下, Elixir 中的条件语句也是函数. 它们可以被插入或链接到其他条件的表达式部分. 这允许我们使用链式的条件语句来解决上面的问题:

代码语言:javascript复制
case foo() do
  {:ok, res} ->
    case bar(res) do
      {:ok, res2} -> do_something_with_result(res2)
      {:error, err} -> handle_error(err)
	end
  {:error, err} -> handle_error(err)
end

尽管只有两个类似的调用操作, 但代码的复杂度是急剧上升的. 如果再添加一个类似的调用操作, 代码将会变得不可读:

代码语言:javascript复制
case foo() do
  {:ok, res} ->
    case bar(res) do
	  {:ok, res2} ->
      case baz(res2) do
        {:ok, res3} -> do_something_with_result(res3)
        {:error, err} -> handle_error(err)
      end
	  {:error, err} -> handle_error(err)
	end
  {:error, err} -> handle_error(err)
end

使用 with 去拯救一下

这就是 with 运算符非常方便的地方. 它的基本形式类似于上面 case 的链式例子, 但在某种程度上, 它的功能也类似于管道操作符. 看看这个:

代码语言:javascript复制
with {:ok, res} <- foo(),
     {:ok, re2} <- bar(res)
     {:ok, re3} <- baz(re2) do
  do_something_with_result(res3)
end

这可以解释为, “按顺序执行所有以逗号分隔的操作, 如果前一个操作是匹配的, 则执行下一个操作. 最后, 运行 do/end 块中的代码”. 这看起来比之前的版本更简洁和可读, 而且它还有另一个很大的优势. 它允许工程师优先关注正常的业务场景. 有些人可能想知道, 如果任何以逗号分隔的操作返回的是 {:error, err} 元组, 会发生什么情况. 答案是, 将返回第一个不匹配的操作表达式. 简单地说, 如果我们不关心非 ok 的结果, 那么我们也可以留下正常的路径, 把它留给调用者来处理最终结果.

如果你使用过 Phoenix,你可能会想起,这正是它的 fallback actions 的工作方式.

代码语言:javascript复制
defmodule MyController do
  use Phoenix.Controller

  action_fallback MyFallbackController

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- Blog.fetch_post(id),
         :ok <- Authorizer.authorize(current_user, :view, post) do

      render(conn, "show.json", post: post)
    end
  end
end

这个 Fallback Controller 的例子来源于 official Phoenix docs

with/else

如果我们想要自己关注这些副作用 (side effects), with 提供了一个扩展版本给我们使用:

代码语言:javascript复制
with {:ok, res} <- foo(),
     {:ok, res2} <- bar(res) do
  do_something_with_res(res2)
else
  {:error, {:some_error, err}} -> handle_some_error(err)
  {:error, {:some_other_error, err}} -> handle_some__other_error(err)
  default -> handle_something_completely_unexpected(default)
end

NOTE: 请记住,虽然基础的 with 形式在匹配失败时不会抛出错误,但在使用 else 时,你必须详尽地匹配所有的情况.

什么使用不应该使用 with

用 else 处理单个模式匹配的场景

这将使代码比你需要的更难以阅读. 代码如下:

代码语言:javascript复制
with {:ok, res} <- foo() do
  do_something_with_res(res)
else
  {:error, {:some_error, err}} -> handle_some_error(err)
end

可以使用更具有可读性的 case 块处理:

代码语言:javascript复制
case foo() do
  {:ok, res} -> do_something_with_res(res)
  {:error, {:some_error, err}} -> handle_some_error(err)
end

相关文章

  • Elixir: Thoughts on the with Statement

0 人点赞