简体   繁体   English

在Haskell中有记录的替代方案吗?

[英]Is there an alternative to records in Haskell?

I am looking for a better alternative to the standard records which are just auto generated accessor functions over tuples. 我正在寻找一个更好的标准记录替代品,它只是自动生成的元组上的访问器函数。 The problem is the naming issues where records with the same names of their fields get same accessor functions. 问题是命名问题,其中具有相同字段名称的记录获得相同的访问器功能。

I am using lens with the makeFields TH function. 我正在使用具有makeFields TH功能的镜头 This lets me do something like this: 这让我做这样的事情:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data Person = Person { _personFirstName :: String
                     , _personLastName :: String
                     , _personEmail :: String }

data Corp = Corp { _corpName :: String
                 , _corpEmail :: String }

makeFields ''Person
makeFields ''Corp

main = 
  let myGuy = Person "Test" "Guy" "email@account.com" 
      myCorp = Corp "ABC" "sales@abc.com" in
      putStrLn $ "personal email: " ++ (myGuy^.email) ++
                 "corp email: " ++ (myCorp^.email) 

What is happening here is that the makeFields function creates type-classes HasName and HasEmail that have a single member (name, or email). 这里发生的是makeFields函数创建具有单个成员(名称或电子邮件)的类型类HasName和HasEmail。 It also creates instances for them for these types. 它还为这些类型创建实例。 MultiParamTypeClasses and Functional Dependencies make it possible to "overload" these members for different data types. MultiParamTypeClasses和Functional Dependencies使得可以为不同的数据类型“重载”这些成员。

makeFields by convention is going to do this for each record constructor that begins with an underscore, and the name it will choose for the field is the balance of the constructor name beginning at the first capital letter. 按惯例,makeFields将为以下划线开头的每个记录构造函数执行此操作,并且它将为该字段选择的名称是从第一个大写字母开始的构造函数名称的余额。 So _corpEmail makes an "email" field. 所以_corpEmail会生成一个“电子邮件”字段。

Now these "fields" are lenses, and there are a lot of functions that you can apply with them. 现在这些“领域”都是镜头,你可以使用很多功能 To simply get the value from a record you could use the "view" function, eg: 要简单地从记录中获取值,您可以使用“视图”功能,例如:

view email myGuy

There is an inverse of this function called (^.), which takes a record as the first parameter and the lens as the second. 这个函数的反函数称为(^。),它将记录作为第一个参数,镜头作为第二个参数。 I personally prefer this style in many cases but I still use view sometimes as well, particularly in a points-free style. 在很多情况下我个人更喜欢这种风格,但我有时也会使用视图,特别是在无点风格中。

Lens is a LOT bigger than just this feature - I personally find this feature compelling but really the most important thing it does for you is enabling you to compose these lenses to make updates deep in data structures. 镜头比这个功能更大 - 我个人觉得这个功能引人注目,但它真正为你做的最重要的事情是让你能够组合这些镜头来深入了解数据结构。 Composition is simply with the (.) operator like any other function; 使用(。)运算符组合就像任何其他函数一样; so if we imagine my example had a _corpPresident :: Person field, I could do this: 所以如果我们想象我的例子有一个_corpPresident :: Person字段,我可以这样做:

 putStrLn $ myCorp^.president.email 

Or - to change this nested value : 或者 - 更改此嵌套值:

let newCorp = set president.email "changedemail@address.com" myCorp 

Or same thing (with more operator madness) : 或者同样的事情(更多运营商的疯狂):

let newCorp = myCorp & president.email .~ "changedemail@address.com"

There is a lot more and I've hardly scratched the surface myself. 还有更多,我自己几乎没有触及表面。 The hackage docs are good but I think the best way to get started is with the field guide which is in the readme of the Github repository . hackage文档很好,但我认为最好的入门方法是使用Github 存储库自述文件中的字段指南。

My policy for namespace conflicts is to put conflicting types in separate modules. 我对命名空间冲突的策略是将冲突类型放在单独的模块中。 Then you can use Haskell's namespacing tools to resolve conflicts. 然后,您可以使用Haskell的命名空间工具来解决冲突。 However, I wish Haskell had a way to define multiple namespaces within a single module. 但是,我希望Haskell能够在单个模块中定义多个名称空间。

From the Haskel 98 report 来自Haskel 98的报告

Operations using field labels are described in Section 3.15. 使用字段标签的操作在3.15节中描述。 A data declaration may use the same field label in multiple constructors as long as the typing of the field is the same in all cases after type synonym expansion. 数据声明可以在多个构造函数中使用相同的字段标签,只要在类型同义词扩展之后所有情况下字段的键入都相同。 A label cannot be shared by more than one type in scope. 标签不能由范围内的多个类型共享。 Field names share the top level namespace with ordinary variables and class methods and must not conflict with other top level names in scope. 字段名称与普通变量和类方法共享顶级名称空间,并且不得与范围中的其他顶级名称冲突。

So no, there isn't a way around it. 所以不,没有办法绕过它。

I usually do something like 我经常这样做

data Person = Person
    { personName :: String
    , personAge :: Int
    } deriving (Eq, Show)

data Pet = Pet
    { petName :: String
    , petAge :: Int
    } deriving (Eq, Show)

class Living a where
    name :: a -> String
    age :: a -> Int

instance Living Person where
    name = personName
    age = personAge

instance Living Pet where
    name = petName
    age = petAge

But to be honest I rarely need (or use) a type class like that. 但说实话,我很少需要(或使用)类似的类型。 It does show how you can use the same "accessor" on different record types. 它确实显示了如何在不同的记录类型上使用相同的“访问器”。

A Vinyl library introduces one solution. 乙烯基库引入了一种解决方案。 To quote the authors: 引用作者:

Vinyl is a general solution to the records problem in Haskell using type level strings and other modern GHC features, featuring static structural typing (with a subtyping relation), and automatic row-polymorphic lenses. Vinyl是Haskell中记录问题的一般解决方案,使用类型级别字符串和其他现代GHC功能,具有静态结构类型(具有子类型关系)和自动行多态透镜。 All this is possible without Template Haskell. 没有Template Haskell,这一切都是可能的。

If however the memory footprint is of concern for you, I'd suggest going with Gabriel's proposal, since Vinyl records are more heavyweight. 然而,如果你的内存占用空间很大,我建议你选择加布里埃尔的提议,因为黑胶唱片的重量级更重。

In general, the Records problem is a notorious issue and great solutions to it have been proposed since the late 90s (can you imagine?). 一般来说,记录问题是一个臭名昭着的问题,自90年代末以来就提出了很好的解决方案(你能想象吗?)。 But since implementing them turned out to be hard, noone ever got to it. 但是,由于实施它们变得困难,没有人能够做到这一点。 Recently I initiated a related discussion on Reddit , you can get some interesting info there. 最近我发起了关于Reddit的相关讨论 ,你可以在那里得到一些有趣的信息。 Turns out, some work is being done at the time at least concerning the namespacing problem and the results of it are expected to see the world some time soon. 事实证明,当时正在做一些工作,至少涉及命名空间问题,预计它的结果很快就能看到世界。

The simplest way around it is to enable the DisambiguateRecordFields extension. 最简单的方法是启用DisambiguateRecordFields扩展。 That doesn't change much at all, it uses basically Haskell98 data declarations but allows you to re-use field names. 这根本没有太大变化,它基本上使用Haskell98数据声明,但允许您重用字段名称。 I've found it to work quite nicely, particularly if you go one step further to RecordWildCards . 我发现它工作得很好,特别是如果你更进一步到RecordWildCards

带有DisambiguateRecordFields的NamedFieldPuns,但仅限于函数并允许绑定,而不是任意表达式: http//www.haskell.org/ghc/docs/7.2.1/html/users_guide/syntax-extns.html

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM