2

The below instances for Vehicle are illegal due to overlapping instances:

data Ford = Ford
data Ferrari = Ferrari

class Car c where
  accelerate :: c -> String

instance Car Ford where
  accelerate _ = "Broom broom I'm a Ford"

instance Car Ferrari where
  accelerate _ = "Rrrr I'm a fast red Ferrari!"

data Bowing747 = Bowing747
data JetFighter = JetFighter

class Plane p where
  takeoff :: p -> String

instance Plane Bowing747 where
  takeoff _ = "My passengers and I are taking off!"

instance Plane JetFighter where
  takeoff _ = "RAWW I'm a loud jet fighter!"

class Vehicle v where
  go :: v -> String

instance (Car c) => Vehicle c where
  go = accelerate

instance (Plane p) => Vehicle p where
  go = takeoff

main = return ()

I'm obviously approaching this in a very object orientated way, and perhaps this is where I'm going wrong.

So how in Haskell can I have a structure like below that allows me to call generic functions in a hierarchy without:

  1. Keeping in sync repeated sets of instances.
  2. Requiring extra "tag" arguments to be added at the call site.

That being said, I've got no need for Car, Plane or Vehicle to be clases, nor Ford, Ferrari etc to be defined how they are, as long as I can call go (some vehicle) and get the same result, and clients can add new cars, planes, and vehicle types without repeating myself.

The vehicles for each type need to be open to users to add their own. The vehicle types themselves could be closed, I'd prefer if they weren't though.

4
  • 1
    If Car and Plane are the same as Vehicle (accelerate = go and takeoff = go), then why even have the Car and Plane type classes? If they aren't necessarily the same as Vehicle, then e.g. trying to provide the Vehicle instance for all Planes is misdirected and you'd be better off with class Vehicle p => Plane p where takeoff :: p -> String; takeoff = go. Commented Feb 14, 2015 at 7:48
  • What if they're not? Commented Feb 14, 2015 at 7:57
  • 1
    class (Eq a) => Num a where When I write this, it says that type a must be an instance of Eq. In your case you are similarly saying Vehicle to be an instance of Plane and also Vehicle to be an instance of Car, which is not possible. This is not overlapping instance error. Overlapping instance error can be removed by using {-# LANGUAGE TypeSynonymInstances, OverlappingInstances #-} (although it's not recomended). Commented Feb 14, 2015 at 8:24
  • I think it should be Vehicle at the top and then both Car and Plane def's should contain Vehicle as a constraint. Class vehicle can define both methods and whichever is appropriate can be chosen by Plane or Car Commented Feb 14, 2015 at 8:26

3 Answers 3

2

In Haskell, you usually don't model OOP by using typeclasses. Instead, you can just write plain data types that contain functions. In your code, a vehicle is anything that has a go method, which returns a String given a Vehicle. So let's define a datatype that expresses this:

data Vehicle = Vehicle { go :: String }

Now it's very easy to make new vehicles. For example, to convert a Plane to a Vehicle you could write a function plane:

data Plane = Plane { takeoff :: String }

plane :: Plane -> Vehicle
plane p = Vehicle { go = takeoff p }

Or you can make a Car type:

data Car = Car { accelerate :: String }

car :: Car -> Vehicle
car c = Vehicle { go = accelerate c }

If you want, you could even make a ToVehicle typeclass so that you can use the same function to convert Car -> Vehicle as you use to convert Plane -> Vehicle and functions can be generic in which type of Vehicle they accept (without forcing the user to convert to a Vehicle first)

class IsVehicle v where
  toVehicle :: v -> Vehicle
instance IsVehicle Car where
  toVehicle = car
instance IsVehicle Plane where
  toVehicle = plane

But I'd suggest to avoid the typeclass if you can. It's better to rethink your design in this case if you want to do this in Haskell.

This approach isn't exactly OOP because you do not have real subtyping (you cannot pass a Car where a Vehicle was expected, if you don't use the typeclass, so you have to manually convert first), but if you adapt your design to this style I found that is enough in practice. So I'd suggest not to try to translate an OOP-based structure to Haskell, but instead thinking of ways to solve this in the Haskell way.

If you are OK with fixing the types of Vehicles, you can even use Haskell's sum types to express it even simpler:

data Vehicle = Car String | Plane String

This approach is quite different to your original approach, as you don't have separate types here anymore, but maybe it fits your use case if you adopt your architecture a little.

Sign up to request clarification or add additional context in comments.

Comments

1

First things first: every language has a different approach at programming. Translating a concept which lies at the core of language X into language Y will often lead to a very poor design. This is because the nice features of Y not present in X are neglected, while trying to put square pegs into round holes.

OOP in Haskell simply does not fit well.

And, for the sake of fairness, many features of Haskell don't fit well in most other languages.

That being said, a common attempt to do some OOP in Haskell is through existential types. E.g.

data Car where
   Car :: state -> (state -> String) -> Car
       -- ^^^^^ the internal representation
                -- ^^^^^^^^^^^^^^^^^ a method

However, in many cases, encoding OOP with existentials is just an anti-pattern. Luke Palmer describes this in a famous blog post. (While I don't fully agree with some points, I find the general argument solid.) For instance, the type above is isomorphic to the simpler type

data Car where
   Car :: String -> Car
       -- ^^^^^^ the method result

and because of laziness, the above works just as well as the original.

Subtyping being non-existent in Haskell, you have to up-cast your types into Car, up-casting meaning converting the "object" into its Car form. Type classes can mitigate this, but often up-casting is not that annoying.

Finally, let me confess I find an OOP a nice but vastly overrated approach to modelling. Just because a few "big" languages are OO-centered, does not mean that OO is the unquestionably best way to model everything.

1 Comment

+1 for mentioning Luke Palmer' blog post. The data approach is better than type class (ab)use.
-1
data Ford = Ford
data Ferrari = Ferrari

class Vehicle v where
    go :: v -> String

class (Vehicle v) => Car v where
    accelerate :: v->String

instance Vehicle Ford where
     go = accelerate

 instance Car Ford where
     accelerate _ = "Vrrom I am ford"

Similarly for plane

class (Vehicle v) => Plane v where
    takeoff :: v-> String

   instance Vehicle Bowing747 where
        go = takeoff

   instance Plane Bowing747 where
      takeoff _ = "taking off from plane"

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.