turtle
turtle

Reputation: 8073

Iterating over custom data types in Haskell

I have a custom data type that looks like this:

data Circle = Circle
        { radius                   :: Float
        , xPosition                :: Float
        , yPosition                :: Float
        }

I want to be able to write a scale function that can take a given circle and change its size like this:

aCircle = Circle 1.5 1 1
scaleFn aCircle 10

The desired output for this example with scale of 10 would be:

Circle 15 10 10

How can I create a function where I can iterate over each field and multiple the values by a constant? In my actual use case I need a way to map over all the fields as there are many of them.

Upvotes: 2

Views: 1123

Answers (2)

chi
chi

Reputation: 116139

You can use "scrap your boilerplate":

import Data.Generics

data Circle = Circle
    { radius                   :: Float
    , xPosition                :: Float
    , yPosition                :: Float
    }
    deriving (Show, Data)

circleModify :: (Float -> Float) -> Circle -> Circle
circleModify f = gmapT (mkT f)

Intuitively, above, mkT f transforms f into a function which is applicable to any type: if the argument of mkT f is a Float, then f is applied, otherwise the argument is returned as it is. The newly constructed general function is called a "transformation": the T in mkT stands for that.

Then, gmapT applies the transformation mkT f to all the fields of the circle. Note that is a field contained, say, (Float, Bool) that float would be unaffected. Use everywhere instead of gmapT to recursively go deeper.

Note that I'm not a big fan of this approach. If for any reason you change the type of a field, that change will not trigger a type error but gmapT (mkT ...) will now simply skip over that field.

Generic programming can be convenient, but sometimes a bit too much, in that type errors can be silently transformed into unexpected results at runtime. Use with care.

Upvotes: 3

leftaroundabout
leftaroundabout

Reputation: 120711

Scaling by a factor is generally a vector space operation. You could do the following:

{-# LANGUAGE TypeFamilies, DeriveGeneric #-}

import Data.VectorSpace
import GHC.Generics (Generic)

data Circle = Circle
        { radius                   :: Float
        , xPosition                :: Float
        , yPosition                :: Float
        } deriving (Generic, Show)

instance AdditiveGroup Circle
instance VectorSpace Circle where
  type Scalar Circle = Float

main = print $ Circle 1.5 1 1 ^* 10

(result: Circle {radius = 15.0, xPosition = 10.0, yPosition = 10.0}).

(requires vector-space >= 0.11, which has just added support for generic-derived instances.)

However I should remark that Circle as such is not really a good VectorSpace instance: adding two circles doesn't make any sense, and scaling by a negative factor gives a bogus radius. Only define such an instance if your real use case follows the actual vector space axioms.

What you really want for a type like Circle is something like diagrams' Transformable class. But I don't think there's any automatic way to derive an instance for that. In fact, since diagrams has – unfortunately IMO – switched from vector-space to linear, something like this has become considerably tougher to do even in principle.

Upvotes: 3

Related Questions