1

I'm trying to implement 4th order Runge-Kutta in Haskell, but I find it difficult to use the Haskell type system for this task. Can someone help? I wish to change the 'State' and 'DState' types into typeclasses in the following code:

data State = State Double deriving (Show)
data DState = DState Double deriving (Show)

update :: State -> DState -> State
update (State x) (DState y) = State (x+y)

add :: DState -> DState -> DState
add (DState x) (DState y) = DState (x + y)

scale :: Double -> DState -> DState
scale h (DState x) = DState (h*x)


update_rk4 :: State -> (Double -> State -> DState) -> Double -> Double -> State
update_rk4 y f t h = update y (scale (h*(1.0/6.0)) s) where
  s = add k1 (add s2 (add s3 k4))
  s2 = scale 2 k2
  s3 = scale 2 k3
  k1 = f t y
  k2 = f (t+0.5*h) ( update y (scale (0.5*h) k1) )
  k3 = f (t+0.5*h) ( update y (scale (0.5*h) k2) )
  k4 = f (t+h) ( update y (scale h k3) )

It seems difficult to formulate the type classes since State and DState are intertwined in the sense that specific instance of State requires a specific instance of DState.. Or is there possibly some other approach that I fail to see?

tero
  • 153
  • 4

2 Answers2

9

This isn't really something for which you want to roll you own type classes. Haskell isn't an OO language where you make classes for everything, rather classes are supposed to capture "deep mathematical concepts". In this example, the very common "concept" is that you can add up differential changes, and sure enough such classes exist already.

update :: (AffineSpace st, d ~ Diff st) => st -> d -> st
      -- note that idiomatic argument order would be `d -> st -> st` instead.
update = (.+^)

add :: VectorSpace d => d -> d -> d
add = (^+^)

scale :: (VectorSpace d, h ~ Scalar d)
   => h -> d -> d
scale = (*^)


update_rk4 :: (AffineSpace st, d ~ Diff st, VectorSpace d, h ~ Scalar d, Fractional h)
     => st -> (h -> st -> d) -> h -> h -> st
     -- again, more idiomatic order is `(h -> st -> d) -> h -> h -> st -> st`.

As to why I recommend putting the st argument last: it's common to apply functions partially in Haskell, and η-reduce the "pipelining argument". In this example, it's quite likely that you want to re-use a particular "RK4-stepper", and if the st argument is last you can do that simply with

simStep :: ParticularState -> ParticularState
simStep = update_rk4 f t h
 where f t y = ...
       ...

If you had to already bind a y variable in simStep y = update_rk4 y f t h, you would then need to either shadow it with the f declaration, or disambiguate with an awkward f t y' = .... It's not a big gain in this case, but if you apply the η-reduction idea consistently it can considerably clean up your overall code.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
0

You can use a multi-parameter typeclasses with functional dependencies, although these are not standard Haskell 98, but GHC extensions. These allow you to define a typeclass whose methods don't determine all of the type parameters, which would otherwise be ambiguous.

For example,

class RK4 state dstate | dstate -> state where
    update :: state -> dstate -> state
    add :: dstate -> dstate -> dstate
    scale :: dstate -> dstate -> dstate

Without the functional dependency, add and scale would be ambiguous because calls to them would not fix the state type, making it impossible to resolve the typeclass constraint.

See further examples, tutorials and discussion at the above link. See also a comparison of functional dependencies vs type families, which is the approach taken in the other response.

Mark Brown
  • 546
  • 3
  • 8