haskell 如何抑制同构于void的记录字段的丢失字段警告?

0ve6wy6x  于 2023-10-19  发布在  其他
关注(0)|答案(1)|浏览(95)

假设我有一个像下面这样的数据类型:

data File' key generated value = File
{
  id :: key Int,
  md5Hash :: generated Text,
  contents :: value Text,
  description :: value Text
  .
  .
}

那么我就可以得到这样的结果

type FileCreate = File' (Const Void) (Const Void) Identity
type FileUpdate = File' Identity (Const Void) Maybe
type FileLookup = File' Identity (Const Void) (Const Void)
type File = File' Identity Identity Identity

但有件烦人的事。
假设我想做一个文件查找,所以我这样做:

FileLookup { id = Identity 1234 }

然后我会得到关于未分配记录字段的警告。我想保持这些警告,因为如果我这样做:

FileCreate { description = Identity "This file hasn't had 'contents' assigned so this should be an error" }

那么这是一个错误,我希望警告提醒我这一点。但是在FileLookup的情况下,其他字段是Const Void。无论如何,它们都没有有效值,因为Const Void是新类型的Void
我的选择似乎是:
1.创建一个函数fileCreate :: Text -> Text -> FileCreatefileUpdate :: Int -> Text -> Text -> FileUpdatefileKey :: Int -> FileLookup等,但这似乎是样板,我失去了很好的记录语法。
1.我可以使用GHC扩展'record patterns'来创建单独的模式,类似于第1点,但这仍然是很多样板文件,特别是因为我必须为我定义的每个新数据类型创建3或4个专门的记录模式同义词。
在我的实际问题中,我可能有很多这样的“文件”,如数据库,但可能只有大约5个模式(插入,更新,查找等)。我很乐意为这些模式做一些样板,只要我不需要为每个新的数据类型重复它。
我很乐意使用泛型,如果它有帮助。如果可能的话,我想避免使用Template Haskell,但如果这是唯一的方法,我想我会处理的。但理想情况下,我喜欢这样的东西,

{-# PRAGMA DontWarnIfThisParticularTypeOrANewtypeOfThisParticularTypeIsNotInitialisedInARecord #-}
data MyConstVoid a -- no constructors

只使用MyConstVoid而不是Const Void(我很乐意这样做)。我就是找不到那样的警报消音器。

polkgigr

polkgigr1#

为什么我不认为这是一个伟大的设计
问题是这些字段未初始化的(这意味着它们包含bottom)。你依赖于它们包含bottom,因为Const Void没有非bottom的居民。你希望GHC理解你的意图,这样它就可以警告 * 一些 * 未初始化的字段,而不是其他字段。
我想我可以理解为什么“这个字段不能初始化(除了底部)”可能会被期望抑制一个关于不初始化字段的警告。问题是,这实际上并不符合Void的用法。
特别是,你的类型说getConst . value应用于FileLookup * 应该 * 产生Void类型的值。我们都同意,一旦你有了一个Void,你就可以使用absurd :: Void -> a将它转换为你喜欢的任何类型。例如,我可以使用你建议的类型编写这个程序:

import Data.Functor.Identity
import Data.Void
import Data.Text
import Control.Applicative

data File' key generated value = File {
  id' :: key Int,
  md5Hash :: generated Text,
  contents :: value Text,
  description :: value Text
}

type FileCreate = File' (Const Void) (Const Void) Identity
type FileUpdate = File' Identity (Const Void) Maybe
type FileLookup = File' Identity (Const Void) (Const Void)
type File = File' Identity Identity Identity

wat :: FileLookup -> anything
wat = absurd . getConst . contents

main :: IO ()
main = do
  let q :: [Maybe (Either () [Double])]
      q = wat $ File { id' = 123 }
  print q

唯一的编译时问题是你想禁用的警告:

Main.hs:26:17: warning: [-Wmissing-fields]
    • Fields of ‘File’ not initialised:
        md5Hash :: Const Void Text
        contents :: Const Void Text
        description :: Const Void Text

FileLookup调用contents对于类型检查器来说是完全没问题的,只要我们能处理结果Const Void(这很容易做到,因为我们有absurd)。因此,如果你想要的功能存在,这个程序将编译警告免费,然后在运行时爆炸。Main: Main.hs:26:17-34: Missing field in record construction contents
Void通常用于标记不可能发生的sum类型的情况。例如,Either Void b是一个只能包含Right (_ :: b)形式的值的类型;要构造一个Left Void,我们首先需要一个Void类型的值来应用Left。2然后either absurd是处理这样的Either Void b值的一种方法,依赖于either永远不会接受它的Left分支(因此永远不会实际应用absurd到任何东西)的假设,因为这样的值永远不会存在,并通过要求它具有Void类型来检查这种情况是“不可能的”(不像处理像error "will never happen"这样的“不可能”情况,当类型改变时,它会很高兴地转换为运行时错误,这样不可能的情况现在就可能了)。
在纯粹的 product 类型中看到它不太常见,因为这将表明FileCreate(etc)值永远不会存在,而不是表明它们的某些字段永远不会被使用。
因此,我认为不太可能有任何具体的支持你的习惯用法内置到编译器中,根据字段的类型以不同的方式处理“未初始化字段”警告。Void并不是设计用来这样使用的。
事实上,它是Void而不是任何其他特定类型并没有真正做任何事情。你从中得到的唯一用处是,如果有人不小心试图填写FileLookupcontents,他们几乎肯定会得到一个类型错误,告诉他们这个错误,因为Const Void不会匹配通常在contents字段中的类型(如果有人试图读取这样的字段,情况也是一样的)。但是Const ()也不会匹配他们期望的类型; Const MyUnexportedType也不会,甚至像Const [Maybe (Either () [Double])]这样任意和愚蠢的东西也不会。简单地让字段未初始化并不能真正利用Void是无人居住的这一事实;你可以不初始化任何其他不太可能有用的类型的字段,并获得几乎完全相同的类型安全性。
1当然,Left undefined可以很好地构造一个使用Left构造函数的Either Void b值。我们基本上相信,有一个“君子协定”,不做这样的事情,虽然;通常很难以那种方式“意外地”绕过类型检查器,而Void专门用于支持这样的编程风格,即我们试图确保所有内容都是完整的,编译器检查不变量。
所以我觉得你应该怎么做?
也许这只是您在这里给出的简化示例,但我也没有真正看到您从这个模式中获得了“好”的语法。FileLookup { id = 123 } * 看起来不错,但实际上并不是这样工作的; FileLookup类型;记录创建语法与构造函数一起使用。所以你实际上必须使用File { id = 123 };没什么能证明这是一个查找。FileDelete可能也只需要一个id,因此看起来也像File { id = 123 }
此外,{ id = 123 }位看起来很好,因为Identity有一个Num示例。还有一个IsString示例,所以如果你使用的是OverloadedStrings,你的Text字段可能看起来也不错。但是过去的任何事情都会让你开始编写像{ flag = Identity True }这样的东西,如果你想让这些字段连接到处理普通IntText值的任何东西,你仍然必须处理添加和删除Identity Package 器,即使你可以在源代码中使用不可见的文字Identity Package 器。

实际上,模式同义词版本(其中未使用的字段基于()而不是Void)在实践中使用会更好。并且,为每个数据类型的每个操作创建模式同义词并不比为每个数据类型的每个操作创建类型同义词多多少。这里有一个快速的例子,我敲了:

{-# LANGUAGE PatternSynonyms, OverloadedStrings, DuplicateRecordFields #-}

import Control.Applicative
import Data.Functor.Identity
import Data.Text

-- same as before
data File' key generated value = File {
  id' :: key Int,
  md5Hash :: generated Text,
  contents :: value Text,
  description :: value Text
}

-- GHC couldn't figure out the constraints with a plain deriving clause on the
-- data type; this isn't important, it's just so I could demo usage in main
deriving instance (Show (key Int), Show (generated Text), Show (value Text))
  => Show (File' key generated value)

-- Unnecessary, but a decent amount of boilerplate reduction, and more easily
-- conveys the "disallowed" sense.
type X = Const ()
pattern X = Const ()

-- Note that here the id' value is a real Int, not an Identity Int. The
-- pattern can transparently apply/remove the Identity wrapper for us.
-- It's entirely up to you whether you do that, but I sure would.
-- (Using the real File constructor makes the Identity visible, of course)
pattern FileLookup :: Int -> File' Identity X X
pattern FileLookup { id' } = File (Identity id') X X X

-- Note that the type signatures for the pattern synonyms are optional
-- if you're averse to boilerplate; I've included them here for clarity
pattern FileUpdate :: Int -> Maybe Text -> Maybe Text -> File' Identity X Maybe
pattern FileUpdate { id', contents, description } = File (Identity id') X contents description

main :: IO ()
main = do
  print $ FileLookup { id' = 123 }
  print $ FileUpdate { id' = 123, contents = Just "foo", description = Nothing }

这是它打印的内容,你可以看到它填充在

File {id' = Identity 123, md5Hash = Const (), contents = Const (), description = Const ()}
File {id' = Identity 123, md5Hash = Const (), contents = Just "foo", description = Nothing}

如果你遗漏了一个字段,比如FileUpdate { id' = 123, contents = Just "foo" },那么你当然会得到一个未初始化的字段警告:

Main.hs:36:11: warning: [-Wmissing-fields]
    • Fields of ‘FileUpdate’ not initialised:
        description :: Maybe Text

注意:

一个足够大的问题,我不想在代码示例的注解中隐藏它:当然,具有记录语法的模式同义词定义了它们自己的字段和选择器函数。所以我必须打开DuplicateRecordFields来允许这一点,如果你试图使用id'作为选择器,你几乎肯定会得到一个模糊性错误。
我可能真正要做的是在一个单独的模块中用DuplicateRecordFieldsNoFieldSelectors定义模式同义词,然后在使用站点上使用DisambiguateRecordFields。如果在一个模块中使用记录 * 更新 * 语法(而不是模式匹配或记录创建),并且在作用域中同时使用真实的类型和一个模式同义词(或两个模式同义词),那么您只需担心对它们进行限定。
或者,NoFieldSelectors也可以是真实的类型,并且只使用显式模式。我定义的模式同义词不仅仅是不使用未使用的字段,它们实际上 * 强制 * 未使用的字段是Const ()。如果你直接使用File编写模式(不匹配未使用的字段)或调用字段选择器函数来使用这些记录(大概是在实际执行操作的模块中),那么有人可以初始化 all 字段并在任何操作中使用该记录,而没有类型安全性。但是,如果FileLookup等模式在 use 站点和创建站点上使用,那么这将被捕获为错误。(虽然如果所有东西最终 * 完全 * 分离,那么拥有一个记录就没有多大意义了;每个模式同义词的单独数据类型将更简单。大概有 * 一些 * 级别,您可以一般地处理这些事情,而不关心其中的操作类型)

相关问题