简体   繁体   中英

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.

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM