在 Haskell 中定义新类型与持久类型的惯用方法

Raz*_*mov 4 postgresql rest haskell servant

我有一种代表持久记录的类型。我想要一个非常相似的类型来表示应该发布以创建新记录的数据。

这是完整的类型:

data Record = Reading
  { id: UUID
  , value: String
  ...
  }
Run Code Online (Sandbox Code Playgroud)

“新”类型与减去“id”相同,该“id”将由数据库自动生成。我如何定义这个类型?我正在使用servant 来定义API。

我当前的策略是在类型和所有字段前加上“new”前缀,这有效,但对于多字段模型来说是多余的。我还看到了嵌套策略,其中我有一个共同的共享类型。我也考虑过将 id 设为可选,但我真的不想发布它。

Ben*_*Ben 5

我使用过的另一种方法是定义没有 ID 的记录,并使用可以向任何内容添加 ID 的单个通用包装类型。

像这样的东西(我使用术语“key”而不是“id”主要是为了避免与内置id函数发生冲突,部分原因是“Keyed”比“Ided”或“WithId”或其他任何东西有更好的响度):

newtype Key a = Key UUID

data Keyed a = Keyed
  { key :: Key a
  , value :: a
  }

data Student = Student
  { name :: Text
  , course :: Course
  , ...
  }
Run Code Online (Sandbox Code Playgroud)

我的推理是,类型的值Student 本身就是一个不可变的值。如果您更改该记录的名称,您只需计算一个不同的 Student值,而不是像“学生更改了名字”这样的概念的表示。我们通常通过添加一个 ID 值来解决这个问题,该值应该唯一对应于真实学生的更大更抽象的概念,这样我们就可以判断两个学生记录何时是关于同一个真实学生的(这样其他记录就可以引用)也是真正的学生)。

但是,像这样关联身份值是原始记录数据之上的附加(且重要)附加功能,并且该功能通常依赖于服务器或数据库的集中位置。但它也是一个单独的功能,可以应用于任何数据记录。id/key 在逻辑上并不是学生记录的一部分,它是我们系统的一个功能,允许在不同时间存在的不同学生记录与现实世界中随时间变化的学生的更大概念相关联。因此,我发现通过添加 id 为与外部概念关联的记录以及仅记录本身设置不同类型非常有用。

然后我会在 API 中使用Student,其中客户端谈论某个学生的详细信息(例如发布数据以指示服务器创建新的学生记录)。我可以Key Student在客户仅处理识别特定学生的情况下使用(例如获取客户已经知道其 ID 的学生的当前详细信息),以及Keyed Student在已分配的上下文中处理记录内容时ID(例如发布更新时)。

但这种分离不仅仅用于对需要 id/key 字段存在或不存在的 API 进行类型检查。实际上,我在客户端和服务器代码库中使用这两种类型,而不仅仅是为了两者之间的通信。客户端级代码通常无法对 ID 进行任何明智的操作,除了从服务器接收 ID 并将其原样传回之外,但确实需要实际处理 ID 才能正确形成请求,因此我不能只是隐藏ID字段完全来自客户端。我发现能够编写不会搞乱 ID 处理的部分代码非常有用,因为它们从来没有被赋予包含 ID 的类型。例如,用于编辑学生详细信息的表单几乎肯定应该只处理Student,因为它不应该允许编辑 id (一个很好的副作用是这样可以轻松地重用相同的表单来创建新学生和编辑现有学生)。

乔建议的港币模式同样适用于创建这种类型的分离。然而,Keyed类型(Key如果您喜欢幻像类型参数来帮助捕获混淆)和关联的帮助程序只需编写一次,而 HKD 模式则适用于每个记录。我发现 HKD 模式有时也会妨碍编写其他实例,因为除非您专门为预期变体之一编写实例,否则您总是有一个为f UUID未知类型键入的字段f,这很难使用;最麻烦的表现可能是简单deriving Showderiving Eq不再有效;您需要使用独立派生来更明确地了解派生实例的约束。通过包装类型添加 id 的方法,普通记录和Keyed包装都是 bog 标准的简单数据类型。

(请注意,Joe 链接的文章使用了一个示例,其中将单个较高种类的f参数统一应用于每个字段,以轻松生成可能缺少所有字段的变体类型,以便处理表单验证等事务。这是与拥有单个字段(您希望数据类型的变体存在或不存在该字段)的用例完全不同。如果您喜欢 HKD 技术,那么很容易想出其他几个需要更高种类类型的不同用例参数应用于不同的领域,当尝试同时支持多个这些用例时,您必须添加多个更高级的类型参数,您可以因不同的原因而改变这些参数,一切都会变得更加混乱。 HKD 技术我非常喜欢的一种,但我发现最好在非常“包含”的类型中使用,例如用于处理表单的类型)

id-wrapper 方法的主要缺点是:

  1. 您确实必须编写一些额外的代码,在处理较大可变概念的系统部分和仅处理简单数据本身的部分之间的边界处应用和删除包装器。一些简单的辅助功能(或镜头)大有帮助,但它仍然存在。
  2. 现有的外部系统通常不是这样构建的,因此,如果您与这些系统进行交互,那么不匹配可能会有点令人厌烦。(例如,如果您使用外部 JSON API,则需要相当自定义的实例(例如ToJSON a => ToJSON (Keyed a)翻译),并且在最坏的情况下,您可能无法使用单个通用实例来处理它)。当您同时控制客户端和服务器并且可以编写适合您的数据类型的 API,而不是编写适合现有 API 的数据类型时,它的效果最佳。
  3. 应用和移除包装器会产生性能成本。在许多情况下,它可以忽略不计,但它不是零。