Type classes vr.s functions

So, style question for people, if I can. I have a certain problem- basically, I have a bunch of functions which need a special function, of type a -> Foo say. And a bunch of other functions which can define that function on some type of interest, and then what to call the first batch of functions. I can do this either by defining a type class, something like: class Fooable a where toFoo :: a -> Foo or I can simply have all the functions which need a toFoo take an extra agrument. Performance really isn't that important here, so it's really a matter of style- which approach would people prefer in this case? Brian

On 2008 Dec 20, at 20:20, Brian Hurt wrote:
class Fooable a where toFoo :: a -> Foo or I can simply have all the functions which need a toFoo take an extra agrument. Performance really isn't that important here, so it's really a matter of style- which approach would people prefer in this case?
A third possibility is to use the simple Reader monad ((->) r). -- brandon s. allbery [solaris,freebsd,perl,pugs,haskell] allbery@kf8nh.com system administrator [openafs,heimdal,too many hats] allbery@ece.cmu.edu electrical and computer engineering, carnegie mellon university KF8NH

On Sat, Dec 20, 2008 at 6:20 PM, Brian Hurt
So, style question for people, if I can. I have a certain problem- basically, I have a bunch of functions which need a special function, of type a -> Foo say. And a bunch of other functions which can define that function on some type of interest, and then what to call the first batch of functions. I can do this either by defining a type class, something like: class Fooable a where toFoo :: a -> Foo or I can simply have all the functions which need a toFoo take an extra agrument. Performance really isn't that important here, so it's really a matter of style- which approach would people prefer in this case?
And it doesn't matter as the performance would be the same in the two cases also. My general rule of thumb is to always write combinators first, since they do not suffer the composability limitations that typeclasses do (rougly typeclasses perform a proof search which is subject to restrictions to ensure decidability, whereas with combinators you provide the proof, so there are no such restrictions). Then typeclass instances can be trivially defined in terms of the combinators. Note that the other way around is not usually possible. So eg.: module Foo where type Fooify a = a -> Foo int :: Fooify Int int = ... list :: Fooify a -> Fooify [a] list = ... -- then, if determined that this would be convenient class Fooable a where toFoo :: Fooify a instance Fooable Int where toFoo = int instance (Fooable a) => Fooable [a] where toFoo = list toFoo ... Luke
Brian
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

Brian Hurt wrote:
So, style question for people, if I can. I have a certain problem- basically, I have a bunch of functions which need a special function, of type a -> Foo say. And a bunch of other functions which can define that function on some type of interest, and then what to call the first batch of functions. I can do this either by defining a type class, something like: class Fooable a where toFoo :: a -> Foo or I can simply have all the functions which need a toFoo take an extra agrument. Performance really isn't that important here, so it's really a matter of style- which approach would people prefer in this case?
For issues of style, I would say to use type-classes. However, this isn't strictly a question of style. As Luke Palmer mentions there are differences of power between the two. In particular, imagine that you have two different and valid ways to convert the same type into Foo; which do you choose? With the continuation/combinator/argument approach this is a non-issue since you can just pass in the one you need. With type-classes it's tricky since they're the same type, which leads to hacks with newtype wrappers or phantom types. If there is guaranteed to be only one valid transformation from any given type into Foo, then type-classes make your intentions clear and they never run into this issue. If more than one valid transformation could exist for some type, then the extra argument is cleaner. Note that when I say "any given type" I mean the domain of values along with its semantic connotations. For instance, there's a straightforward way of 'converting' Double into an instance of Num. However, if we semantically interpret the values of Double as if they were in the log-domain, then there is a different way to convert it into Num[1]. But really, these are different types because they have different semantics even if they have the "same" values. Though much abused, newtype declarations are intended to capture exactly this distinction between values and semantics, and they make it straightforward for the Haskell type-checker to see that they are indeed different types. [1] http://hackage.haskell.org/cgi-bin/hackage-scripts/package/logfloat -- Live well, ~wren

On Tue, 23 Dec 2008, wren ng thornton wrote:
In particular, imagine that you have two different and valid ways to convert the same type into Foo; which do you choose? With the continuation/combinator/argument approach this is a non-issue since you can just pass in the one you need. With type-classes it's tricky since they're the same type, which leads to hacks with newtype wrappers or phantom types.
If there is guaranteed to be only one valid transformation from any given type into Foo, then type-classes make your intentions clear and they never run into this issue. If more than one valid transformation could exist for some type, then the extra argument is cleaner.
In this case, there is gaurenteed to be only one valid transformation. Basically, I have a number of similar data structures (which enforce different constraints, which is why they're not all the same data structure), and the function in question converts the specific (constraint-enforcing) data structures into a general (non-constraint-enforcing) data structure on which I can perform generic algorithms. To be more specific, I'm writing a compiler in Haskell (what a shock), and the source data structures are the parse trees after various transformations- for example, after the lambda lifting phase, the parse tree should not have lambda expressions in them at all (they should have all been lifted to top-level functions). So, while the before-lambda-lifting data structure has a Lambda constructor, the after-lambda-lifting data structure doesn't, thus enforcing the constraint that lambda lifting removes all (non-top-level) lambda expressions. But I want to be able to get all free variables of a given expression, both before and after lambda lifting, so I just define a function to convert both types into a common generic representation that I can write a "get free variables" function to work on. At this point, I realize that I'm being stupid and way over thinking things. Haskell is a *lazy* language- I'm still wrapping my head around the implications of that statement. My original thinking was that the conversion function would be a one-level conversion, i.e. the data structure would be like: data Generic a = UnaryOp uop a | BinaryOp a bop a | If a a a ... i.e. I'd only convert the root node, and the child nodes would still be the original data structure. So I'd need to pass around a function of the form: a -> Generic a which is my conversion function. But what I'm doing here is hand-reimplementing a lazy conversion of the data structure- which I get for free anyways. So what I should do is define the data structure like: data Generic = UnaryOp uop Generic | BinaryOp Generic bop Generic | If Generic Generic Generic ... Then all I need to do is to write the pure conversion function a -> Generic, and then run the generic algorithm over the data structure. That gives me the exact behavior I'm looking for, without either (explicitly) passing a conversion function around or playing with fancy typeclasses. Pardon me while I go beat my head against the wall. Brian
participants (4)
-
Brandon S. Allbery KF8NH
-
Brian Hurt
-
Luke Palmer
-
wren ng thornton