首页 理论教育 函数式编程入门:处理非纯函数

函数式编程入门:处理非纯函数

时间:2023-11-20 理论教育 版权反馈
【摘要】:单子有丰富的数学理论,类似的概念还有函子、应用函子、幺半群。为了使其工作,我们需要将带有值和函数的单子传递给bind函数。在这个例子中,当我们将数据绑定到具有错误单子的函数时,bind仅在数据被标记为成功时执行该函数;当它失败时,bind会忽略对函数的调用。我们将使用MonadEx,因为它是目前GitHub上最受关注的库。success/1函数将值包装在成功上下文中,error/1将值包装在错误上下文中。generate_question/1返回一个字符串值。如果返回false,则显示错误并要求用户重试。

函数式编程入门:处理非纯函数

错误单子(error monad)是一种数据结构,可帮助你组合可能导致错误的函数。它允许将函数置于清晰的序列中,在一处集中处理错误。如果函数有可能有意外结果,它可以用来减少条件代码。当代码中有许多按顺序放置的函数并且其中一些函数可能失败时,你可以使用它。例如,有五个必须按顺序执行的函数,但其中一些函数容易出错。

您可能听说过单子(monad),它在静态类型编程语言领域很有名,比如Haskell。单子有丰富的数学理论,类似的概念还有函子(functor)、应用函子(applicative)、幺半群(monoid)。但不要担心,我们只关心如何在实践中使用单子。

通常,单子使用属性包装值,属性可以提供有关值的更多信息。这样,就能将函数与值组合起来进行自动判断。例如,我们可以在值为错误时自动跳过函数执行。看看图7-1中的例子:

图7-1 自动跳过函数执行的例子

使用错误单子,当值有错误时,就能自动决定跳过函数执行,将错误带到集中处理的地方。为了使其工作,我们需要将带有值和函数的单子传递给bind函数。bind知道如何组合函数和值。它调用函数,传递从单子中提取的值。在这个例子中,当我们将数据绑定到具有错误单子的函数时,bind仅在数据被标记为成功时执行该函数;当它失败时,bind会忽略对函数的调用。

让我们在游戏程序中试验单子策略。我们不打算从头实现一个单子。我们会使用现成的库(Monad[2]、Towel[3]、witchcraft[4] 和 MonadEx[5])。这些库各有优缺点。我们将使用MonadEx,因为它是目前GitHub上最受关注的库。MonadEx的README文档包含许多有用的链接,可以了解更多有关单子的信息。转到mix.exs文件并添加MonadEx:

MonadEx库将被下载和编译,然后就可以在我们的应用程序中使用了。让我们使用IEx尝试类似图7-1中的场景。运行iex -S mix并尝试:

我们使用use Monad.Operators指令将~>>运算符(bind运算符)添加到我们的会话中。这个运算符就是我们之前讨论的bind函数。它左侧需要一个单子,右侧需要一个函数。我们从Monad.Result导入函数。Result单子与我们之前讨论的Error单子相同。这个库的作者给它取了一个不同的名称,但它们是相同的。success/1函数将值包装在成功上下文中,error/1将值包装在错误上下文中。~>>运算符在成功上下文中执行值,而在错误上下文中跳过值。

让我们重写DungeonCrawl.CLI.BaseCommands来试试单子策略。在模块的开头,我们将从MonadEx导入一些函数:

以下是我们从Monad.Result导入的函数:

● success/1将给定值包装在标记为成功的单子中。

● return/1将给定值包装在标记为成功的单子中。

● error/1将给定值包装在标记为失败的单子中。

● success?/1,当给定结果单子被标记为成功时返回true,否则返回false。

success/1和return/1是一样的。它有两个名字,因为有时在语义上使用一个比另一个好。现在让我们重写基本命令函数来返回单子:(www.xing528.com)

在display_options/1中,使用return函数来包装结果成功时的选项列表。这是必要的,因为在MonadEx库中列表是一种单子。如果我们将列表传递给绑定操作符,它将尝试提取列表中的项。在这个例子,我们不希望提取。这就是我们将列表包装在结果单子中的原因。generate_question/1返回一个字符串值。使用MonadEx库,将字符串值包装在结果单子中是可选的。绑定函数不会尝试提取非单子的值。更改以下函数以返回单子:

在parse_answer/1函数中解析整数可能会返回一个错误。我们用带有模式匹配的case进行检查。如果解析结果错误,就用error/1返回附带消息的错误结果。如果解析结果是有效数字,就用success/1返回包装了数字的成功结果。find_option_by_index/2遵循相同的逻辑。当它与nil值匹配时,返回错误结果;当它与数字匹配时,返回成功的结果。这样修改后,我们就可以更新ask_for_option/1来利用它了:

我们使用函数return/1将选项列表包装在结果单子中来启动管道。这是因为,如果列表是单子,那么它将在绑定操作符中触发不同的操作。我们使用绑定运算符~>>作为管道对接函数。它的工作方式与我们熟知的管道操作符|>非常相似。主要区别是~>>的右侧需要一个匿名函数。~>>将自动决定是否应该执行下一个函数。如果值标记有错误,~>>会跳过下一个函数;如果值标记为成功,~>>就执行下一个函数。有了这个操作符,我们就可以创建一个清晰的函数执行序列并在它之后处理错误,而不是在错误发生时立即处理。

我们将管道执行的返回值放在结果变量中。使用success?/1函数检查结果是否成功。如果返回true,则返回所选选项,访问value属性。如果返回false,则显示错误并要求用户重试。你可以运行mix start查看效果。

使用单子处理无效选项的主要优点在于:有清晰的愉快路径的函数管道;我们将错误处理放在一个统一的位置;函数始终返回一个值,返回标记错误或成功的一致数据结构。缺点是Elixir没有内置的单子,因此我们需要选择一个库,而且单子库的语法看起来可能与Elixir的语法不一致。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈