4

I need help translating an OO concept into Haskell.

Imagine a Vehicle class and Car and Truck subclasses, with a driveOneMile method that returns a double representing the total fuel used. Each call to driveOneMile also changes the internal state of the vehicle.

This is what I've done so far in Haskell (since there are no instance variables in Haskell, it seems I have to create my own "state" types):

type CarState = (Double,Double)
initialCarState = (0,0)

driveCarOneMile :: CarState -> (Double,CarState) --Double: total fuel used
driveCarOneMile s = ...


--the internal state of trucks is more complex. needs three Doubles
type TruckState = (Double,Double,Double)
initialTruckState = (0,0,0)

driveTruckOneMile :: TruckState -> (Double,TruckState)
driveTruckOneMile s = ...

In a similar way I could construct other vehicles and their "drive" functions.

This is how I would drive a car twice:

fuelFor2Miles = fst $ driveCarOneMile $ snd $ driveCarOneMile initialCarState

  1. Am I doing this correctly?
  2. If not, how would you correct it?
  3. How can I use the above (or your corrected way) to take a collection of many cars, trucks, and other vehicles, drive each of them 10 times, and get the corresponding [Double] list of the total fuel used? (In OO this would be a simple matter of throwing the vehicles into a list, calling the method on each of them 10 times, and putting the total fuel used into a new list.)
stackoverflowuser
  • 1,157
  • 9
  • 18
  • 10
    It's better to solve problems in Haskell directly than try to figure out how to do OO in Haskell. "take a collection of many cars, trucks, and other vehicles, drive each of them 10 times," - I have a hard time believing this corresponds to anything except a beginner's OO problem class, designed to teach inheritance. You'd get further by solving Haskell beginners problems designed to teach you functional programming. Try [Learn You a Haskell for Great Good](http://learnyouahaskell.com/) for starters. – AndrewC May 07 '14 at 16:14
  • 3
    It's certainly possible to solve this problem in Haskell, and you may get some nice answers, but OO exercises aren't the best way of learning Haskell. – AndrewC May 07 '14 at 16:16
  • 2
    http://stackoverflow.com/questions/20184286/object-oriented-programming-in-haskell/20188103#20188103 – leftaroundabout May 07 '14 at 20:40

4 Answers4

8

Classes in Haskell are quite different from OO classes, and using them as if you were writing OO code in most cases makes things more complicated than they should be. In particular, as soon as you begin thinking about "[taking] a collection of many cars, trucks, and other vehicles" in OO terms you go straight down the rabbit hole (as pointed out by Haskell Antipattern: Existential Typeclass).

It is likely that you don't really need a class at all to model your different types of vehicles. You might define

data Vehicle = Car CarState | Truck TruckState

and then

driveOneMile :: Vehicle -> (Double, Vehicle)

using pattern matching to distinguish between different vehicles.

Even if you really need something more class-like (e.g. you are writing a library and want users to supply their own vehicles) that doesn't necessarily mean you need heterogeneous collections. You can give Vehicle a single constructor and add fields to it corresponding to the class methods, so that the class becomes a type and the class instances become values (that is the approach advocated by the antipattern article). You might also have a Vehicle class with a toGeneralVehicle :: a -> GeneralVehicle method, and have the common behaviour defined in terms of the GeneralVehicle type.

P.S.: The idea behind signatures such as TruckState -> (Double,TruckState) is sound. In fact, I'd say that you have accidentally discovered the State type! State is just a convenient abstraction to make the state-passing plumbing implicit. If you are curious, look for questions and tutorials about the State monad.

duplode
  • 33,731
  • 7
  • 79
  • 150
2

My initial gut reaction to your question is to implement this using the state monad, which duplode hints at in his answer. Here is a way to also accomplish what you want where the code somewhat imperative.

As duplode suggests, I would also define

data Vehicle = Car CarState | Truck TruckState

Then, you could define driveOneMile as

driveOneMile :: State Vehicle Double
driveOneMile = do
    vehicleState <- get
    case vehicleState of
         Car carState -> do
             ...
             return totalFuelUsed
         Truck truckState -> do
             ...
             return totalFuelUsed

Now, in your example, where you drive two miles, don't you want to sum up the amount of fuel used for each mile? The way you have it, it only returns the amount of fuel used for the second mile only.

With the state monad, driveTwoMile or even driveNMiles n is pretty trivial:

driveTwoMile :: State Vehicle Double
driveTwoMile = do
    firstMileFuelUsage <- driveOneMile
    secondMileFuelUsage <- driveOneMile
    return (firstMileFuelUsage + secondMileFuelUsage)

driveNMiles :: Int -> State Vehicle Double
driveNMiles n = do
    fuelUsages <- replicateM n driveOneMile
    return (sum fuelUsages)

As for a list of vehicles, I am not sure what the best way to do this would be, but the following is a way of doing this (I am using lists as a monad here):

type DrivingDistance = Int

driveManyVehicles :: [Vehicle] -> [DrivingDistance] -> [(Double, Vehicle)]
driveManyVehicles v d = do
    (currentVehicle, drivingDistance) <- zip v d
    return (runState (driveNMiles drivingDistance) currentVehicle)
Emil
  • 2,098
  • 11
  • 25
  • If you have questions about how this actually works, feel free to ask. The state monad isn't very intuitive the first time you see it. I would, however, suggest this tutorial on state monads: http://mvanier.livejournal.com/5406.html – Emil May 07 '14 at 17:52
0

If you really want a class, it would like this:

class Vehicle a where
  driveOneMile :: a -> Double

data Truck = Truck -- Fill in the details...
data Car   = Car   -- Fill in the details...

instance Vehicle Truck
instance Vehicle Car

But as the author of previous answer said - it is probably better to implement both as different constructors of Vehicle datatype.

Michal Gajda
  • 603
  • 5
  • 13
  • 2
    This is a typeclass, which is something very different from an OOP class. I don't actually think OP was asking about typeclasses. – kqr May 07 '14 at 18:26
0

This is how I would model your scenario:

I would not glom together Car and Truck to fit under one sum type. Sum types are not intended to describe some commonality of data between types (but rather a disjoint union of cases) You could capture the data commonality via a type class (although its not saving you much for just two types). I would then define your collection of Vehicles as lists of each type.

type Miles = Double
type Gallons = Double

data Truck = Truck { tid :: Int, truckTankGallons :: Gallons, tmpg :: Double  } deriving (Show)
data Car   = Car   { cid :: Int, carTankGallons   :: Gallons, cmpg :: Double  } deriving (Show)

class GasVehicle a where
  mpg :: a -> Double
  gasGallons :: a -> Gallons
  changeGasAmt :: Gallons -> a -> a

instance GasVehicle Car where
  mpg = cmpg
  gasGallons = carTankGallons
  changeGasAmt g c = Car (cid c) g (cmpg c)

instance GasVehicle Truck where
  mpg = tmpg
  gasGallons = truckTankGallons
  changeGasAmt g c = Truck (tid c) g (tmpg c)

milesToGallons :: GasVehicle a => a -> Miles -> Gallons
milesToGallons a m = m / (mpg a)

driveMiles :: GasVehicle a => Miles -> a -> (a, Gallons)
driveMiles m a = (a', dg) where
  dg = (milesToGallons a m)
  a' = changeGasAmt (gasGallons a - dg) a

data Vehicles = Vehicles { cars :: [Car], trucks :: [Truck] } deriving (Show)

driveVehicles :: Miles -> Vehicles -> (Vehicles, Gallons)
driveVehicles m v = (Vehicles cars' trucks', gasUsed) where
  gasUsed = (sum carGallons) + (sum truckGallons)
  (cars', carGallons)     = unzip $ map (driveMiles m) $ cars v
  (trucks', truckGallons) = unzip $ map (driveMiles m) $ trucks v
nlim
  • 287
  • 1
  • 5