Eswink Eswink
  • 首页
  • 热议
    • 话题
    • 网站公告
  • 红蓝对抗
    • 攻防对抗
    • 渗透分析
  • 资源分享
    • 代码发布
    • 其他分享
  • 本站专题
    • 视频集锦
    • WordPress
    • 工程实践
    • 奇闻趣事
    • 活动线报
  • 问答
  • 绿标域名
  • 关于本站
    • 友情链接
  • 注册
    登录
现在登录。
  • 首页
  • 热议
    • 话题
    • 网站公告
  • 红蓝对抗
    • 攻防对抗
    • 渗透分析
  • 资源分享
    • 代码发布
    • 其他分享
  • 本站专题
    • 视频集锦
    • WordPress
    • 工程实践
    • 奇闻趣事
    • 活动线报
  • 问答
  • 绿标域名
  • 关于本站
    • 友情链接
首页 代码发布 小伍关于Elixir中的代数数据类型的详细解答

小伍关于Elixir中的代数数据类型的详细解答

小伍同学 2022-06-01 21:43:10 本文共5248个字,预计阅读时间需要14分钟
1 星2 星3 星4 星5 星 (已有1 点评, 平均得分: 4.00)您还未点评
Loading...

Elixir是一种动态类型语言。Elixir中的类型在程序运行时检查,而不是在编译时检查。如果它们不匹配,则会引发异常。在静态类型语言中,类型是在编译时检查的。这可以帮助我们编写正确、可理解和可重构的代码。但它也引入了对类型的某种关注,将其作为应用程序的基础。一个有趣的概念是使用类型来为您的业务领域建模。在 Haskell、F# 和 OCaml 等语言中,这通常是使用代数数据类型 (ADT) 完成的——它们通过将类型与乘积 (AND) 和总和 (OR) 类型聚合来构建复合数据类型。在静态分析工具 Dialyzer 的帮助下,您可以使用 ADT 来限制应用程序允许状态的数量。这减少了错误溜进来的机会。

小伍关于Elixir中的代数数据类型的详细解答图片

在 Elixir 中使用 Dialyzer 进行类型声明

在 Elixir(和其他 BEAM 语言)中,检查类型规范通常使用Dialyzer完成。Dialyzer 不同于 Haskell、OCaml 甚至 TypeScript 的类型系统。Dialyzer 需要向您证明您的代码不正确,而不是您向编译器证明您的代码是正确的。

这使得 Dialyzer 的要求相当宽松。如果类型有办法工作,Dialyzer 会假设您知道自己在做什么,并且类型确实可以工作。但是推理类型和捕捉偶尔出现的错误仍然很有用。

Elixir中透析器的快速介绍

你可以在 Elixir 中使用 Dialyzer,通过为你的函数添加类型规范@spec。

小伍关于Elixir中的代数数据类型的详细解答图片1
#          (1)      (2)           (3)
  @spec plus_one(integer) :: integer
 
  def plus_one(x), do: x + 1
end

在这里,我们只写函数的名称 (1) 及其类型作为参数 (2),然后是函数的返回类型 (3)。

您可以在 Elixir 的文档中找到要使用的基本类型列表。

您还可以通过创建自己的类型别名@type。为此,您需要提供别名的名称 (1) 及其类型 (2)。

#         (1)        (2)
  @type counter :: integer

如果您使用 Elixir 语言服务器,这就是您需要做的所有事情:您的插件/扩展将通知您 Dialyzer 不喜欢的任何规范。否则,您需要运行mix dialyzer任务来检查您的类型。

你可以在 Elixir 的网站和Elixir 的 typespec 文档中找到更多关于Dialyzer 的使用。

现在我们可以指定我们的 Elixir 代码,让我们深入研究代数数据类型。

代数数据类型

小伍关于Elixir中的代数数据类型的详细解答图片2

虽然这个名字听起来很吓人(哦,代数👻),但代数数据类型相对简单。

本节将重点介绍 ADT 的两个主要部分:乘积 (AND) 和和 (OR) 类型。

产品类型

产品类型无处不在。产品类型只是具有两个或多个字段的类型,每个字段都包含一个数据类型。您也可以将它们视为 AND 类型。

例如,元组是一种产品类型:

@type tuple(a, b) :: {a, b}

它采用两种数据类型——a和b——并返回一个包含这两种数据类型的类型。

在 Elixir 中,我们还使用带有命名字段的产品类型——结构。

defmodule Person do
  defstruct first_name: "Gints", last_name: "Dreimanis"
end

让我们看看这个例子中的类型:

@type t() :: %__MODULE__{
        first_name: String.t(),
        last_name: String.t()
      }

通常,您可以将类型视为可能值的集合。例如,布尔值有两个可能的值::true和:false。交通灯颜色的类型具有以下三个可能值之一::green、:yellow和:red。

如果我们有两个 sizea和b的类型,那么这些类型的 product 类型将包含a * b值——这些类型的 size 的乘积。如果您制作布尔值和交通灯类型的产品类型——例如,将布尔值和交通灯放在一个元组中——您将有2 * 3 = 6可能的值。

{:green, :true}
{:yellow, :true}
{:red, :true}
{:green, :false}
{:yellow, :false}
{:red, :false}
小伍关于Elixir中的代数数据类型的详细解答图片3

总和类型

历史上不太常见的类型是 sum 类型。

与产品类型相比,总和类型为您提供两个(或更多)选项之一。您也可以将它们视为 OR 类型。

我们也在 Elixir 中使用这些。例如,结果元组是求和类型。

@type result(a, b) :: {:error, a} | {:ok, b}

在结果元组中,我们可能会遇到类型错误a 或类型成功b。

或者,例如,我们可以为可选值设置一个 sum 类型。

@type optional(a) :: :error | {:ok, a}

但它也可以仅用于制作替代品列表。

@type direction :: :north | :west | :south | :east

如果您在 sum 类型中列出两种类型,则生成的类型可以从一组值或另一组值中选择一个类型。因此,它的大小通常是这些类型的大小之和。

在使用 Dialyzer 时,上述情况可能并不总是正确的:您可以将两个重叠的集合放在一起。为了使陈述成立,它们需要用它们来自的集合进行标记——我们上面定义的结果类型就是一个很好的例子。

这就是人们通常所说的代数数据类型。

代数数据类型还有更多内容

通过将总和和乘积放在一起,我们就有了类似于我们在学生时代就知道和喜爱的代数:乘法、总和和变量。

当然,代数类型不仅限于此:还有递归、指数等。如果您想更深入地研究该主题,请查看它们在 Haskell 等语言中的外观。

小伍关于Elixir中的代数数据类型的详细解答图片4

代数数据类型如何帮助领域建模

我们 Elixir 程序员通常不会考虑求和类型。在 Elixir 中对域建模的主要工具是 struct,这是一种具有命名字段的产品类型。

虽然这对大多数事情来说已经足够了,但有时使用 sum 类型也是有益的。

让我们看一个例子。

自定义看板

假设我们需要创建一个自定义看板问题的表示。

我们的问题可能处于以下状态之一:

  • 搜索受让人:在这种情况下,它应该既没有受让人也没有审阅者
  • 尚未开始:在此和以下状态下,它应该有一个受理人但没有审阅者
  • 进行中
  • In review:在这个和下面的状态下,应该有一个assignee和一个reviewer
  • 完毕

所有问题也都有名称和描述。

一开始,人们可能会想使用一个简单的结构。

defmodule Issue do
  defstruct name: "",
            description: "",
            state: :searching_for_assignee
            assignee: nil,
            reviewer: nil
end

但是正如我们在产品类型部分看到的,一个简单的产品类型有很多可能的值,其中一些可能不符合我们的要求。

例如,我们可以创建一个搜索受让人但仍有受让人和审阅者的问题。

iex(1)> %Issue{name: "wrong issue", description: "not good at all", state: :searching_for_assignee, assignee: "Jorge Luis Borges", reviewer: "Gabriel García Márquez"}
%Issue{
  assignee: "Jorge Luis Borges",
  description: "not good at all",
  name: "wrong issue",
  reviewer: "Gabriel García Márquez",
  state: :searching_for_assignee
}

虽然通常可以避免这样做,但让它变得不可能更简单。我们对此有句俗语:“使非法国家无法代表”。

为此,我们需要创建一个 sum 类型,涵盖我们想要允许的所有状态。它将使我们能够通过用值的总和代替值的乘积来消除一些错误的状态。

首先,让我们将state、assignee和reviewer字段合并为一个字段:state。

defstruct name: "",
          description: "",
          state: :searching_for_assignee

之后,让我们定义一个 sum 类型state,它将包含我们指定的选项。

让我们再看看他们。我们的问题可能处于以下状态之一:

  • 寻找受让人
  • 尚未开始,但有受让人
  • 进行中并与受让人
  • 与受让人和审阅者一起审阅
  • 完成,受让人和审阅者留作历史记录

定义一个几乎像这样读取的类型非常容易:

@type state ::
        :searching_for_assignee
        | {:not_started, String.t()}
        | {:in_progress, String.t()}
        | {:in_review, String.t(), String.t()}
        | {:done, String.t(), String.t()}

为了更容易理解,我们可以为受理人和审阅者创建别名。

@type assignee :: String.t()
@type reviewer :: String.t()

现在,该类型看起来与我们的规则列表完全一样。

@type state ::
        :searching_for_assignee
        | {:not_started, assignee}
        | {:in_progress, assignee}
        | {:in_review, assignee, reviewer}
        | {:done, assignee, reviewer}

剩下的就是Issue使用我们的状态类型为模块 struct () 创建一个类型规范。

@type t() :: %__MODULE__{
        name: String.t(),
        description: String.t(),
        state: state
      }

这是完整的模块代码:

defmodule Issue do
  defstruct name: "",
            description: "",
            state: :searching_for_assignee
 
  @type assignee :: String.t()
  @type reviewer :: String.t()
  @type state ::
          :searching_for_assignee
          | {:not_started, assignee}
          | {:in_progress, assignee}
          | {:in_review, assignee, reviewer}
          | {:done, assignee, reviewer}
 
  @type t() :: %__MODULE__{
          name: String.t(),
          description: String.t(),
          state: state
        }
end

现在我们可以测试这种类型规范是否可以阻止我们犯逻辑错误。

我们将创建一个为问题添加审阅者的函数,但我们会在其中添加一个错误:它不会改变问题的状态。我们还将添加一个类型规范。

@spec add_assignee(Issue.t(), assignee) :: Issue.t()
def add_assignee(%{state: :searching_for_assignee} = issue, assignee_name) do
  %{issue | state: {:searching_for_assignee, assignee_name}}
end

Dialyzer 将在此处正确返回类型错误:

lib/issue.ex:21:invalid_contract
The @spec for the function does not match the success typing of the function.
 
Function:
Issue.add_assignee/2
 
Success typing:
@spec add_assignee(%{:state => :searching_for_assignee, _ => _}, _) :: %{
  :state => {:searching_for_assignee, _},
  _ => _
}

这有点神秘,但它基本上意味着Issue.add_assignee没有编译,我们应该调查一下!🙃

如您所见,代数数据类型使我们免于犯错。事实证明,他们并不是真正的可怕怪物,而是朋友。

Elixir应用程序的代数数据类型的好处

为 Elixir 应用程序采用代数数据类型是一个两步决策过程。

第一步是选择使用 Dialyzer 和 typespecs。Dialyzer提供了任何具有静态类型的语言的大部分好处:

  • 更容易发现你在代码中犯的错误。
  • 类型提供了关于代码的额外信息:它做什么以及它操作什么值。这在尝试理解代码时很有帮助。
  • 编写代码后,类型可以确保代码仍然执行相同的操作(类似于测试),因此更容易重构。
小伍关于Elixir中的代数数据类型的详细解答图片5

一旦您将 Dialyzer 用于您的代码库,就应该自然而然地考虑代数数据类型,并带来一些好处:

  • 正如我们在示例中看到的那样,sum 类型尤其可以让您减少可能的状态并使非法状态无法表示。
  • 在你的词汇表中使用 AND 和 OR 可以帮助你以一种即使对于非开发人员(领域专家)也很直观和易于理解的方式构建复合类型。

当然,ADT 只是软件正确性的一种工具——绝对不是灵丹妙药。但总的来说,ADT 对任何使用使用 Dialyzer 的 Elixir 代码库的人来说都是一个有用的概念。

如果您的代码库不使用 Dialyzer,那么您的首要目标应该是引入它,这比在编写类型规范时更改类型的方式要大得多。不幸的是,这项工作超出了本文的范围。

#Elixir#Elixir效率#Erlang#现代编程#编程语言
0
小伍同学
一个即将入土的程序猿
赞赏
小伍同学
评论 (0)
请登录以参与评论。
现在登录。
    发表评论
猜你喜欢
  • Go语言错误处理为什么更推荐使用pkg/errors 三方库?
  • JAVA如何自定义Mysql连接池
  • 如何将震源球绘制在谷歌地球上
  • 如何通过使用CloudFlare的转换规则实现隐藏bucket路径
  • 巧妙应用Docker运行HuggingFace
小伍同学
一个即将入土的程序猿
160
文章
78
评论
176
获赞
小伍同学
23 6月, 2022
Blocksy PRO v1.8.35|专业版Blocksy主题推荐
Copyright © 2020-2022 Eswink. Designed by nicetheme. 川公网安备 51012202000979号 |  萌ICP备20225505号 | 蜀ICP备20002650号-6
当前线路为: 国内线路
本站已安全运行:
本站由酷盾安全提供高防CDN国际线路安全防护服务
友链: Eswink 信息笔记 网站目录 脚本挂机引流赚钱 龙笑天下 HTTP代理 QYV企业商务咨询 镇北府博客 吾爱漏洞 祭夜の咖啡馆 杭州论坛 龙鳞收录网 KIENG博客 Hackyh‘Blog
在线客服
小伍同学
我们将会在24小时内回复您,如果有急事请联系QQ或者微信
12:01
您好,有任何疑问请与我们联系!
公众号

选择聊天工具: