使用相同的内部表示和最小的样板处理多种类型? [英] Handling multiple types with the same internal representation and minimal boilerplate?

查看:129
本文介绍了使用相同的内部表示和最小的样板处理多种类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我在Haskell编写大型程序时,我发现自己常常遇到一个问题。我发现自己经常需要多种不同的类型,它们共享一个内部表示和几个核心操作。



有两种相对明显的方法来解决这个问题。
$ b

一个是使用一个类型类型和 GeneralizedNewtypeDeriving 扩展名。将足够的逻辑放入类型类中以支持用例期望的共享操作。使用所需的表示形式创建一个类型,并为该类型创建一个类型类的实例。然后,对于每个用例,使用newtype为它创建包装,并派生出公共类。



另一种是用幻像类型变量声明类型,然后使用 EmptyDataDecls 为每个不同的用例创建不同的类型。



我主要关心的不是将共享的值内部表示和操作,但在我的代码中有不同的含义。这两种方法都能解决这个问题,但感觉显着笨拙。我的第二个担心是减少所需的样板数量,并且这两种方法都做得很好。

每种方法的优点和缺点是什么?是否有一种技术更接近我想做的事情,提供没有样板代码的类型安全性?解析方案

例子,并没有发现两种方法之间的性能差异,但使用通常会有所不同。

例如,在某些情况下,您有一个泛型类型,其构造函数暴露你想用 newtype 包装来表示一个更具有语义特定的类型。使用 newtype s然后导致呼叫站点,例如,

  s1 = Specific1 $ GeneralBob23 
s2 = Specific2 $ GeneralJoe19

事实上,不同的具体新类型之间的内部表示是相同的。



类型标记方法几乎总是伴随着表示构造函数隐藏,

  data General2 a = General2 String Int 

和使用智能构造函数,导致数据类型定义和调用网站类似,

  mkSpecific1Bob 23 

部分原因是您希望使用某种语法轻的方式指示您想要的标签。如果你没有提供聪明的构造函数,那么客户端代码通常会选择类型注释来缩小范围,例如,

  myValue = General2 String Int :: General2 Specific1 

一旦您采用智能构造函数,您可以轻松添加额外的验证逻辑抓住标签的滥用。幻像类型方法的一个很好的方面是对于有权访问表示的内部代码,模式匹配根本不会改变。

  internalFun :: General2 a  - >一般2 a  - > Int 
internalFun(General2_ age1)(General2_ age2)= age1 + age2

当然,您可以使用带有智能构造函数和内部类的 newtype s来访问共享表示,但我认为这个设计空间中的一个关键决策点是您是否想要保留您的表示构造函数暴露。如果表示的共享应该是透明的,并且客户端代码应该可以自由地使用它希望的任何标签而不需要额外的验证,那么 newtype 包装带有 GeneralizedNewtypeDeriving 工作正常。但是如果你要采用巧妙的构造函数来处理不透明的表示,那么我通常更喜欢幻像类型。


I find myself running into a problem commonly, when writing larger programs in Haskell. I find myself often wanting multiple distinct types that share an internal representation and several core operations.

There are two relatively obvious approaches to solving this problem.

One is using a type class and the GeneralizedNewtypeDeriving extension. Put enough logic into a type class to support the shared operations that the use case desires. Create a type with the desired representation, and create an instance of the type class for that type. Then, for each use case, create wrappers for it with newtype, and derive the common class.

The other is to declare the type with a phantom type variable, and then use EmptyDataDecls to create distinct types for each different use case.

My main concern is not mixing up values that share internal representation and operations, but have different meanings in my code. Both of those approaches solve that problem, but feel significantly clumsy. My second concern is reducing the amount of boilerplate required, and both approaches do well enough at that.

What are the advantages and disadvantages of each approach? Is there a technique that comes closer to doing what I want, providing type safety without boilerplate code?

解决方案

I've benchmarked toy examples and not found a performance difference between the two approaches, but usage does typically differ a bit.

For instance, in some cases you have a generic type whose constructors are exposed and you want to use newtype wrappers to indicate a more semantically specific type. Using newtypes then leads to call sites like,

s1 = Specific1 $ General "Bob" 23
s2 = Specific2 $ General "Joe" 19

Where the fact that the internal representations are the same between the different specific newtypes is transparent.

The type tag approach almost always goes along with representation constructor hiding,

data General2 a = General2 String Int

and the use of smart constructors, leading to a data type definition and call sites like,

mkSpecific1 "Bob" 23

Part of the reason being that you want some syntactically light way of indicating which tag you want. If you didn't provide smart constructors, then client code would often pick up type annotations to narrow things down, e.g.,

myValue = General2 String Int :: General2 Specific1

Once you adopt smart constructors, you can easily add extra validation logic to catch misuses of the tag. A nice aspect of the phantom type approach is that pattern matching isn't changed at all for internal code that has access to the representation.

internalFun :: General2 a -> General2 a -> Int
internalFun (General2 _ age1) (General2 _ age2) = age1 + age2

Of course you can use the newtypes with smart constructors and an internal class for accessing the shared representation, but I think a key decision point in this design space is whether you want to keep your representation constructors exposed. If the sharing of representation should be transparent, and client code should be free to use whatever tag it wishes with no extra validation, then newtype wrappers with GeneralizedNewtypeDeriving work fine. But if you are going to adopt smart constructors for working with opaque representations, then I usually prefer phantom types.

这篇关于使用相同的内部表示和最小的样板处理多种类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆