为什么GHC Haskell不支持重载记录参数名称?

25
我所说的是无法定义以下内容:
data A = A {name :: String}
data B = B {name :: String}

我知道GHC会将其简化为普通函数,解决这个问题的惯用方法是:

data A = A {aName :: String}
data B = B {bName :: String}

class Name a where
  name :: a -> String

instance Name A where
  name = aName

instance Name B where
  name = bName

在写完这个之后,我不太喜欢它...这种类型分类是否可以成为解糖过程的一部分?
当我在编写一些Aeson JSON解析时,我产生了这个想法。虽然对于每个数据类型只需派生FromJSON实例就可以了,但我不得不手动编写所有内容(目前超过1k行)。在数据记录中使用像name或简单的value之类的名称并不罕见。 http://www.haskell.org/haskellwiki/Performance/Overloading提到函数重载会引入一些运行时开销。但我实际上不明白为什么编译器不能在编译时解决这个问题,并在内部给它们分配不同的名称。 这篇来自2012年的SO问题或多或少地说明了历史原因,并指向了2006年的邮件线程。最近有什么改变吗?
即使存在一些运行时开销,大多数人都不会介意,因为大多数代码几乎不需要高性能。
是否有一些隐藏的语言扩展实际上允许这样做?再次我不确定...但我认为Idris实际上可以做到这一点?

顺便说一句:能否有人在SO上添加一个Idris标签和这个问题?也许来自那个社区的某个人可以详细说明。 - fho
恭喜您成为第一个标记idris的问题。如果您想了解更多有关该主题的信息,可以访问GHC WikiReddit上的讨论页面。 - Davorak
谢谢提供链接。 GHC 文档在我的 Google 搜索结果中似乎排名不高。 - fho
1
请注意,对于您特定的Aeson问题,您可以使用Data.Aeson.TH自动生成实例。它允许您规范化键,例如从记录字段名称中去除类型前缀。 - shang
1
@shang 感谢建议。 不幸的是,TH解析器不能处理可选字段,而我的TH-fu水平不足以改变这一点。 - fho
3个回答

4
许多原因,主要是一些小问题。其中一个问题来自更好的答案中提出的问题,仅仅在第一个参数上进行重载无法处理所有有用的情况。
您可以进行“解糖”。
data A { name :: String }
data B { name :: Text   }

进入

class Has'name a b | a -> b where
    name :: a -> b

data A { aName :: String }
instance Has'name A String where
    name :: aName

data B { bName :: Text   }
instance Has'name B Text   where
    name :: bName

但这需要使用 GHC 扩展(功能依赖),而这些扩展还没有成为标准。这将排除仅使用 "name" 进行记录创建、更新和模式匹配的可能性(视图模式可能有所帮助),因为在这些情况下, "name" 不仅仅是一个函数。你可以使用模板 Haskell 实现非常相似的功能。

这并没有涉及到多态更新或者页面上的其他类型类。 - Jonathan Fischoff
如果您需要进行多态更新,那么您应该仍然将“name”定义在类型类中,但请确保它具有类型Functor f => (a -> f b) -> s -> f t(或类似的内容),以便它可以用作镜头。不过,在这种情况下,我通常会倾向于使用不同但较长的名称来表示不同的镜头,通过限定导入方式实现。 - Boyd Stephen Smith Jr.

3

使用记录语法

data A { name :: String }

隐式定义一个函数。
name :: A -> String

如果我们用 { name :: String } 定义了 AB,那么就会出现类型定义冲突的问题,因为 name 在两个类型中都有定义。
name :: A -> String
name :: B -> String

如果我们定义了两种类型,你提出的隐式类型类的工作方式并不清楚。

data A { name :: String }
data B { name :: Text }

那么我们只是把问题转移到了类型类定义冲突上:

class Has'name a where
     name :: a -> String

class Has'name a where
     name :: a -> Text

原则上,这个问题可以通过一种或另一种方式解决,但这只是记录中几个棘手的冲突性质之一。当Haskell被定义时,决定采用简单但有限的支持,而不是尝试设计更雄心壮志和复杂的东西。在不同的时间讨论了记录的几个改进,也有长期的讨论,例如这个Haskell Cafe线程。也许对于Haskell Prime会有所改进。


不行!我们已经知道了。而且拒绝了它!那种脑残的“思考”正是问题所在。他的意思是每个数据类型都有单独的命名空间!就像面向对象编程一样,A.name不会与B.name冲突。(name A)和(name B)也不应该,原因完全相同。我的意思是,它可以将“data A { name :: String }”和“let a = A“ Betty ”in name a”内部解析为“data A {a_Name :: String}”和“let a = A“ Betty ”in aName a”。还有什么问题? - anon
4
@Evi1M4chine:也许在提出建议并要求其他人实施之前,您应该先阅读一些关于这个问题的大量现有讨论,以免提出一些实际上并不起作用的东西。 - C. A. McCann
@Evi1M4chine 那么在你的例子中,name的类型是什么? 它需要具有某种主要类型,否则一般的类型推断会出现严重问题。 现在,如果你不太关心反向类型推断,你可以将其解析为开放的非单射类型族。 - semicolon

0

我发现最好的方法是使用预处理器来解决这个非常愚蠢的问题。

Haskell和GHC使这变得容易,因为整个Haskell解析器都可以作为普通库使用。您可以解析所有文件,根据应用名称函数的数据类型(例如,“data A { name :: String }”和“let a = A“ Betty” in name a”转换为“data A { a_Name :: String }”和“let a = A“ Betty” in aName a”)使用类型解析器进行重命名,并将它们写出以供编译。

但说实话,这应该集成到GHC中。你是对的:这不包括在内是愚蠢的。


但并非所有类型都在编译时已知,因此您必须关注主要类型等问题。在您的示例中,“name”的类型是什么?它必须有一个类型才能很好地与Haskell的类型推断等其他部分配合使用。例如,如果“A”和“B”都是“Monoid”的实例,那么“name mempty”会给您带来什么? - semicolon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接