5

I'm trying Haskell's Gloss module, and I found a a pattern of things required to properly display an object: Its position, dimensions, scale and Picture representation. This seemed like a good use case of typeclasses, so I wrote this up:

class Displayable d where

    toPicture :: d -> Picture

    getDims     :: d -> (Float,Float)
    getPos      :: d -> (Float,Float)
    getScale    :: d -> Float

    setDims     :: (Float,Float) -> d -> d
    setPos      :: (Float,Float) -> d -> d
    setScale    :: Float -> d -> d

    setDims _ d     = d
    setPos _ d      = d
    setScale _ d    = d

    getDims _   = (0,0)
    getPos _    = (0,0)
    getScale _  = 0

I've never used customs typeclasses before though, so I have some questions about their use:

  1. First and foremost, is this situation appropriate for a typeclass?

  2. I noticed that I need a lot of methods for this to work (I'll probably be adding get/setRotation too, so it will grow). Is this typical? Is this a sign that I'm trying to encompass too much?

  3. Should I be defining default definitions like I have? For certain scenarios, I may not need to define a particular characteristic, but would like the object to take advantage of the rest of the class (like needing to rotate a world, or get the dimensions of a "non-physical entity"). I realize though, that if the default definitions aren't accounted for down the road, it could lead to some odd results (like objects that have a 0 scale by default, which may render them invisible).

  4. Are getters/setters the best way to achieve what I'm trying to do (a central interface for manipulating displayable objects)?

(I know that representations of data aren't thought of as objects in Haskell, but given I'm trying to represent something visual, I thought the term object would be appropriate)

Any thoughts would be appreciated.

  • What concrete problem would this solve? – Tom Ellis Nov 5 '14 at 11:00
  • @Tom Ellis Reusability. If I have a standardized interface that the rest of my library uses, I just need to write an instance for any new types, and it will work. – Carcigenicate Nov 5 '14 at 12:08
  • Sure, pehaps I should have been more specific with my question. For example, why is a record of the methods you listed not sufficient to address reusability? Why does it need to be a typeclass? – Tom Ellis Nov 5 '14 at 13:38
  • Iv'e never used a "dictionary of methods" before. It might be sufficient. – Carcigenicate Nov 5 '14 at 13:39
  • I would suggest it. It will make things a lot simpler. – Tom Ellis Nov 5 '14 at 13:43
4

Usually in this sort of situation, you would just create a simple data type or perhaps a record to aggregate the different fields:

data Displayable = Displayable Picture (Float,Float) (Float,Float) Float

Type classes are for when you need polymorphic dispatch to many implementations of the same function. That's certainly not the case for scale and position, for which you'll only have one implementation. Dimensions might have different ways to be calculated, such as based on the natural dimensions of a bitmap, but those differences can almost certainly be accounted for at construction time.

You might have a case for making toPicture part of a type class, but that can likely be mostly handled at construction time as well, then have one function that adds the appropriate transformations on top of a base Picture just before rendering it.

Another possible data structure might be a Picture and a list of transformation functions to apply to it:

data Displayable = Displayable Picture [Picture -> Picture]

This makes it easier to add new kinds of transformations, like rotations or even complex combinations of transforms, but makes it more difficult to do operations like reset the scale back to the default. Perhaps you could add a String description for each transformation. Which is the best will depend on your application. My point in including this example is to show that using function fields can often keep you from resorting to a type class.

It's not that type classes aren't useful. Far from it. It's just that the situations where creating a new one is the best fit are relatively rare. When you do need to create them, they are generally very small and almost never change. If you're thinking you might need to add a function to a type class later, that should make you think twice about using one.

It's okay to have lots of functions to manipulate the data type in more programmer-friendly ways, but you want them to be outside of type classes, if possible. Every time you add a function to a type class, you have to add it to every single instance.

  • My main motivation for making a Typeclass was that if I needed to ever add another type of displayable "thing" (a window, or animation etc.), I would just need to write an instance for it, and it would work with everything I've already written that works with the Typeclass. You're saying though that the typical alternative is just to keep adding constructors to 1 type, and as long as the field names match, it will still work? I'm just tired of rewriting modules for every new situation that I encounter. I'd like to have a solid graphics module that I can rely on across projects. – Carcigenicate Nov 5 '14 at 12:16
3

It's usually a good idea avoid type classes unless it provides a specific benefit that cannot be gained by any other means. This article provides a somewhat extreme view on this, so take it with a grain of salt, but it can help you understand why type classes can be troublesome.

The main issue type classes operate on the type level, which means that trying to do complicated things with type classes will typically require a plethora of Haskell extensions, and sometimes even impossible. in contrast, operating on the value level is much simpler and you don't have to fight with the type checker and type inferencer regularly.

Getters and setters have their places in Haskell, but they are primarily for maintaining interface compatibility and using them excessively will cause a lot of boilerplate. For simple, internal data structures, they might not be worth the extra effort.

In your example, it would be much simpler to implement Displayable as an ordinary data type (as shown in Karl Bielefeldt's example), with the added benefit that you can store all your Displayables in a container without funky extensions like ExistentialTypes. Additionally, you can make use of pattern matching (or the record syntax) to deconstruct your Displayables.


On the issue of default definitions, they should be used cautiously. Additionally, it's probably a bad idea to write defaults that don't work on every instance. It's not a strict guideline, but it really helps to prevent silly mistakes like forgetting to write a function for an instance: without defaults, if you forget something, the compiler will give you a warning, or at worst cause a runtime error; that's not the case if it's defaulted, and if the default behavior is wrong, then your program might end up being subtly broken.


Addendum: a "record of methods" (or "instance dictionary" as it's often called) is actually what Haskell uses to implement type classes under the rug. Such an approach would be something like:

data Displayable d = Displayable
  { display   :: d
  , toPicture :: d -> Picture
  , getDims   :: d -> (Float, Float)
  , getPos    :: d -> (Float, Float)
  , getScale  :: d -> Float
  , setDims   :: (Float,Float) -> d -> d
  , setPos    :: (Float,Float) -> d -> d
  , setScale  :: Float -> d -> d
  }

Any type class can be transformed into this form: this a literal translation of the type class in your question. You probably don't want to use in this exact form though: it's best if you can figure out which parts of Displayable really needs to be abstract, and which parts you can make concrete. Too much flexibility can make the code more complex than it needs to be.

  • 1
    Sorry. I appreciate the link, but I found the other answer more helpful. – Carcigenicate Nov 6 '14 at 1:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.