何时使用类型类,何时使用类型 [英] When to use a type class, when to use a type

查看:114
本文介绍了何时使用类型类,何时使用类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我重新访问了几个月前编写的用于组合搜索的代码,并注意到有一种替代的,更简单的方法来完成我以前使用类型类实现的功能。



具体来说,我之前有一个类型为 search problems 的类型类,它的类型为 s ,类型为 a 的动作(对状态的操作),初始状态,获取(动作,状态)对列表的方式以及测试state是一个解决方案或不是:

  class问题psa其中
initial :: psa - > s
继承者:: p s a - > s - > [(a,s)]
goaltest :: p s a - > s - > Bool

这有点令人不满意,因为它需要MultiParameterTypeClass扩展,并且通常需要FlexibleInstances和可能的TypeSynonymInstances你想创建这个类的实例。它也混乱了你的函数签名,例如。

pre $ pathToSolution :: Problem p => p s a - > [(a,s)]

今天我注意到我可以完全摆脱这个类,使用类型代替,如下所示:

  data问题sa {
initial :: s,
继任者:: s - > [(a,s)],
goaltest :: s - > Bool
}

这不需要任何扩展,函数签名看起来更好:

  pathToSolution :: Problem sa  - > [(a,s)] 

最重要的是,我发现在重构我的代码以使用这个抽象而不是一个类的类,我留下了比以前少15-20%的线。



最大的胜利在于使用类型创建抽象的代码类 - 以前我必须创建新的数据结构,以复杂的方式包装旧数据结构,然后将它们变为 Problem 类的实例(这需要更多的语言扩展) - 大量的代码行来做一些相对简单的事情。重构后,我只有几个函数完成了我想要的功能。



现在我正在查看其余代码,试图找出实例我可以用类型替换类型类,并获得更多胜利。



我的问题是:这种重构不是会在什么情况下工作?在什么情况下,使用类型类而不是数据类型实际上更好?以及如何提前识别这些情况,所以您不必经历昂贵的重构?

$ b

解决方案

考虑一种情况,即类型和类都存在于同一个程序中。该类型可以是该类的一个实例,但这很简单。更有趣的是,你可以从ProClassClass ::(CProblem p s a)=>中写入一个函数 p s a - > TProblem sa



您执行的重构大致等同于手动内联 fromProblemClass 你构造了一个用作 CProblem 实例的东西,并且让每个接受 CProblem 实例的函数接受 TProblem



由于此重构中唯一有趣的部分是 TProblem

这是什么时候工作的?



考虑来自ProblemClass的实现 。你将基本上将类的每个函数部分应用于实例类型的值,并且在该过程中消除对 p 参数的任何引用(这是类型替换)。



任何重构类型类的情况都很简单,它会遵循类似的模式。



什么时候适得其反?



想象一下 Show 的简化版本,只有显示函数定义。这允许相同的重构,应用 show 并用... a String 替换每个实例。很显然,我们在这里失去了一些东西 - 也就是能够处理原始类型并在不同点将它们转换为 String 的能力。 Show 的值是它在各种不相关的类型上定义的。

作为一个经验法则,如果有很多不同的函数,它们是类的实例的特定类型,并且这些函数通常与类相同的代码中使用功能,延迟转换是有用的。如果在单独处理类型的代码和使用该类的代码之间存在明显的分界线,那么转换函数可能更适合,而类型类只是一个小句法方便。如果类型几乎全部通过类函数使用,那么类型类可能完全是多余的。



何时不可能?



顺便说一下,这里的重构类似于OO语言中的类和接口之间的差异;类似地,重构不可能的类型类是许多OO语言中不能直接表达的类型。

这一点,一些你不能轻易翻译的例子,如果有的话,以这种方式:


  • 该类的类型参数仅出现在协变位置中,例如函数的结果类型或非函数值。对于 Monoid 返回,对于 memlet code> Monad


  • 类的类型参数在函数类型中多次出现可能不会使这种事情变得不可能,但这会使事情变得更为复杂。值得注意的罪犯包括 Eq Ord ,以及基本上每个数字类。


  • 非常重要的高级使用,其中的细节我不确定如何固定,但(>> = ) for Monad 在这里是一个显着的罪犯。另一方面,类中的 p 参数不是问题。

  • 对于多参数类型类的非平凡使用,我也不确定如何在实践中固定并且非常复杂,可以与面向对象语言中的多次调度进行比较。同样,你的班级在这里没有问题。


请注意,鉴于上述情况,此重构不是甚至对许多标准类型来说都是可能的,并且对于少数例外情况会起反作用。这不是巧合。 :]



你通过应用这个重构放弃了什么?



你放弃了区分原始类型。这听起来很明显,但它可能很重要 - 如果有什么情况需要控制使用哪个原始类实例类型,则应用此重构会失去某种程度的类型安全性,而您只能通过跳过其他地方使用的相同类型的箍来恢复,以确保在运行时不变。



相反,如果有些情况下您确实需要制作各种实例类型可互换 - 您提到的复杂包装是此类的典型症状 - 通过丢弃原始类型可以获得很大的收益。通常情况下,您并不真正关心原始数据本身,而是关于它如何让您对其他数据进行操作;因此直接使用函数的记录比一个额外的间接层更自然。



如上所述,这与OOP及其最适合的问题类型密切相关,以及表达问题的另一面与ML风格语言的典型代表。


I was revisiting a piece of code I wrote to do combinatorial search a few months ago, and noticed that there was an alternative, simpler way to do something that I'd previously achieved with a type class.

Specifically, I previously had a type class for the type of search problems, which have an states of type s, actions (operations on states) of type a, an initial state, a way of getting a list of (action,state) pairs and a way of testing whether a state is a solution or not:

class Problem p s a where
    initial   :: p s a -> s
    successor :: p s a -> s -> [(a,s)]
    goaltest  :: p s a -> s -> Bool

This is somewhat unsatisfactory, as it requires the MultiParameterTypeClass extension, and generally needs FlexibleInstances and possibly TypeSynonymInstances when you want to make instances of this class. It also clutters up your function signatures, e.g.

pathToSolution :: Problem p => p s a -> [(a,s)]

I noticed today that I can get rid of the class entirely, and use a type instead, along the following lines

data Problem s a {
    initial   :: s,
    successor :: s -> [(a,s)],
    goaltest  :: s -> Bool
}

This doesn't require any extensions, the function signatures look nicer:

pathToSolution :: Problem s a -> [(a,s)]

and, most importantly, I found that after refactoring my code to use this abstraction instead of a type class, I was left with 15-20% fewer lines than I had previously.

The biggest win was in code that created abstractions using the type class - previously I had to create new data structures that wrapped the old ones in a complicated way, and then make them into instances of the Problem class (which required more language extensions) - lots of lines of code to do something relatively simple. After the refactor, I just had a couple of functions that did exactly what I wanted to.

I'm now looking through the rest of the code, trying to spot instances where I can replace type classes with types, and make more wins.

My question is: in what situation does will this refactoring not work? In what cases is it actually just better to use a type class rather than a data type, and how can you recognise those situations ahead of time, so you don't have to go through a costly refactoring?

解决方案

Consider a situation where both the type and class exist in the same program. The type can be an instance of the class, but that's rather trivial. More interesting is that you can write a function fromProblemClass :: (CProblem p s a) => p s a -> TProblem s a.

The refactoring you performed is roughly equivalent to manually inlining fromProblemClass everywhere you construct something used as a CProblem instance, and making every function that accepts a CProblem instance instead accept TProblem.

Since the only interesting parts of this refactoring are the definition of TProblem and the implementation of fromProblemClass, if you can write a similar type and function for any other class, you can likewise refactor it to eliminate the class entirely.

When does this work?

Think about the implementation of fromProblemClass. You'll essentially be partially applying each function of the class to a value of the instance type, and in the process eliminating any reference to the p parameter (which is what the type replaces).

Any situation where refactoring away a type class is straightforward is going to follow a similar pattern.

When is this counterproductive?

Imagine a simplified version of Show, with only the show function defined. This permits the same refactoring, applying show and replacing each instance with... a String. Clearly we've lost something here--namely, the ability to work with the original types and convert them to a String at various points. The value of Show is that it's defined on a wide variety of unrelated types.

As a rule of thumb, if there are many different functions specific to the types which are instances of the class, and these are often used in the same code as the class functions, delaying the conversion is useful. If there's a sharp dividing line between code that treats the types individually and code that uses the class, conversion functions might be more appropriate with a type class being a minor syntactic convenience. If the types are used almost exclusively through the class functions, the type class is probably completely superfluous.

When is this impossible?

Incidentally, the refactoring here is similar to the difference between a class and interface in OO languages; similarly, the type classes where this refactoring is impossible are those which can't be expressed directly at all in many OO languages.

More to the point, some examples of things you can't translate easily, if at all, in this manner:

  • The class's type parameter appearing only in covariant position, such as the result type of a function or as a non-function value. Notable offenders here are mempty for Monoid and return for Monad.

  • The class's type parameter appearing more than once in a function's type may not make this truly impossible but it complicates matters quite severely. Notable offenders here include Eq, Ord, and basically every numeric class.

  • Non-trivial use of higher kinds, the specifics of which I'm not sure how to pin down, but (>>=) for Monad is a notable offender here. On the other hand, the p parameter in your class is not an issue.

  • Non-trivial use of multi-parameter type classes, which I'm also uncertain how to pin down and gets horrendously complicated in practice anyway, being comparable to multiple dispatch in OO languages. Again, your class doesn't have an issue here.

Note that, given the above, this refactoring is not even possible for many of the standard type classes, and would be counterproductive for the few exceptions. This is not a coincidence. :]

What do you give up by applying this refactoring?

You give up the ability to distinguish between the original types. This sounds obvious, but it's potentially significant--if there are any situations where you really need to control which of the original class instance types was used, applying this refactoring loses some degree of type safety, which you can only recover by jumping through the same sort of hoops used elsewhere to ensure invariants at run-time.

Conversely, if there are situations where you really need to make the various instance types interchangeable--the convoluted wrapping you mentioned being a classic symptom of this--you gain a great deal by throwing away the original types. This is most often the case where you don't actually care much about the original data itself, but rather about how it lets you operate on other data; thus using records of functions directly is more natural than an extra layer of indirection.

As noted above, this relates closely to OOP and the type of problems it's best suited to, as well as representing the "other side" of the Expression Problem from what's typical in ML-style languages.

这篇关于何时使用类型类,何时使用类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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