Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Functional object-oriented programming in Haskell

Tags:

haskell

Following the book "Get programming with Haskell" I stumbled over rather confusing exercise. One of the chapters explains how to make simple objects using closures. So, for instance we have a tuple which describes a primitive robot: (name, attack, hp). And using this tuple we can construct a robot like this:

robot (name,attack,hp)  = \message -> message (name,attack,hp)

for instance:

killerRobot = robot ("Kill3r", 25, 200)

And so on, the author explains how to make accessor functions using this structure:

hp (_,_,hp) = hp
attack (_,a,_) = a

getHP aRobot = aRobot hp
getAttack aRobot = aRobot attack

so we can examine how many hit point the certain robot has:

getHP killerRobot

So far so good and I'm not going to rewrite the entire chapter, but I can't catch one further thing. Next we have a function:

damage aRobot attackDamage = aRobot (\(n,a,h) ->
                                      robot (n,a,h-attackDamage))

and another function

fight aRobot defender = damage defender attack
  where attack = if getHP aRobot > 10
                 then getAttack aRobot
                 else 0

which emulates a combat between two robots. So we can write something like that:

gentleGiant = robot ("Mr. Friendly", 10, 300)
gentleGiantRound1 = fight killerRobot gentleGiant
killerRobotRound1 = fight gentleGiant killerRobot
gentleGiantRound2 = fight killerRobotRound1 gentleGiantRound1
killerRobotRound2 = fight gentleGiantRound1 killerRobotRound1
gentleGiantRound3 = fight killerRobotRound2 gentleGiantRound2
killerRobotRound3 = fight gentleGiantRound2 killerRobotRound2

and it works just fine. But when I try to put this into a function, which incapsulates these steps and returns the result of the last step (actually the task is slightly different) I get a whole bunch of errors related to type system I guess. I'll show a simplified version which throws errors:

roundFights rb1 rb2 = 
 let rb2' = fight rb1 rb2
 in fight rb2' rb1

The second fight makes compiler explode with errors. All those functions don't have type signatures intentionally - the same thing inside the book - because it's just one of the introductory chapters and type signatures have not yet explained.

Can someone suggest what's wrong?

Here is the source code:

robot (name, attack, hp) = \message -> message (name, attack, hp)

name (nm, _, _) = nm
attack (_, a, _) = a
hp (_, _, p) = p

getName r = r name
getAttack r = r attack
getHP r = r hp

setName r nm = r $ \(_, a, hp) -> robot (nm, a, hp)
setAttack r a = r $ \(nm, _, hp) -> robot (nm, a, hp)
setHP r hp = r $ \(nm, a, _) -> robot (nm, a, hp)

printRobot r = r $ \(nm, a, hp) -> nm ++ " attack:" ++ show a ++ " hp:" ++ show hp

damage r ad = r $ \(nm, a, hp) -> robot (nm, a, hp - ad)
fight atacker defender = damage defender power where
  power = if getHP atacker > 10
          then getAttack atacker
          else 0

lives = map getHP

roundFights rb1 rb2 = 
   let rb2' = fight rb1 rb2
   in fight rb2' rb1
     

rb1 = robot("Killer", 25, 200)
rb2 = robot("Slayer", 15, 200)

and the errors sheet I get:

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:27:18:
    Occurs check: cannot construct the infinite type:
      t8 ~ ((t7, t8, t8) -> t0) -> t0
    Expected type: ((t7, t8, t8) -> ((t7, t8, t8) -> t0) -> t0) -> t6
      Actual type: ((t7, t8, t8) -> t8) -> t6
    Relevant bindings include
      rb2' :: ((t4, t5, t5) -> t5) -> t8
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:26:8)
      rb2 :: ((t2, t3, t6) -> ((t2, t3, t6) -> t) -> t)
             -> ((t4, t5, t5) -> t5) -> t8
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:25:17)
      rb1 :: ((t7, t8, t8) -> t8) -> t6
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:25:13)
      roundFights :: (((t7, t8, t8) -> t8) -> t6)
                     -> (((t2, t3, t6) -> ((t2, t3, t6) -> t) -> t)
                         -> ((t4, t5, t5) -> t5) -> t8)
                     -> t6
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:25:1)
    In the second argument of `fight', namely `rb1'
    In the expression: fight rb2' rb1

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:30:27:
    No instance for (Num t1) arising from the literal `200'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb1 :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:30:1)
    Note: there are several potential instances:
      instance Num Double -- Defined in `GHC.Float'
      instance Num Float -- Defined in `GHC.Float'
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus three others
    In the expression: 200
    In the first argument of `robot', namely `("Killer", 25, 200)'
    In the expression: robot ("Killer", 25, 200)

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:31:27:
    No instance for (Num t1) arising from the literal `200'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb2 :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:31:1)
    Note: there are several potential instances:
      instance Num Double -- Defined in `GHC.Float'
      instance Num Float -- Defined in `GHC.Float'
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      ...plus three others
    In the expression: 200
    In the first argument of `robot', namely `("Slayer", 15, 200)'
    In the expression: robot ("Slayer", 15, 200)

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:33:8:
    No instance for (Ord t1) arising from a use of `fight'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb2' :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:33:1)
    Note: there are several potential instances:
      instance Integral a => Ord (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      instance Ord () -- Defined in `GHC.Classes'
      instance (Ord a, Ord b) => Ord (a, b) -- Defined in `GHC.Classes'
      ...plus 24 others
    In the expression: fight rb1 rb2
    In an equation for rb2': rb2' = fight rb1 rb2

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:34:8:
    No instance for (Ord t1) arising from a use of `fight'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb1' :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:34:1)
    Note: there are several potential instances:
      instance Integral a => Ord (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      instance Ord () -- Defined in `GHC.Classes'
      instance (Ord a, Ord b) => Ord (a, b) -- Defined in `GHC.Classes'
      ...plus 24 others
    In the expression: fight rb2' rb1
    In an equation for rb1': rb1' = fight rb2' rb1

D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:35:9:
    No instance for (Ord t1) arising from a use of `fight'
    The type variable `t1' is ambiguous
    Relevant bindings include
      rb2'' :: (([Char], t1, t1) -> t) -> t
        (bound at D:\Dropbox\Documents\Work\HS\GetProg\Unit 10\Robot.hs:35:1)
    Note: there are several potential instances:
      instance Integral a => Ord (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
      instance Ord () -- Defined in `GHC.Classes'
      instance (Ord a, Ord b) => Ord (a, b) -- Defined in `GHC.Classes'
      ...plus 24 others
    In the expression: fight rb1' rb2'
    In an equation for rb2'': rb2'' = fight rb1' rb2' Failed, modules loaded: none.
like image 432
BarbedWire Avatar asked Jun 30 '26 23:06

BarbedWire


1 Answers

As far as I can see, your code works fine in an untyped setting (say, Scheme or JavaScript).

In a typed setting it could work, but only if it involves fairly complex types, namely rank-2 types. The type inference engine can not infer those, which must be annotated manually.

To stress the point, let's try to use rank-1 types, only, adding all the annotations. This part compiles fine.

type Robot a = ((String, Int, Int) -> a) -> a

robot :: (String, Int, Int) -> Robot a
robot (name, attack, hp) = \message -> message (name, attack, hp)

name :: (String, Int, Int) -> String
name (nm, _, _) = nm
attack :: (String, Int, Int) -> Int
attack (_, a, _) = a
hp :: (String, Int, Int) -> Int
hp (_, _, p) = p

getName :: Robot String -> String
getName r = r name
getAttack :: Robot Int -> Int
getAttack r = r attack
getHP :: Robot Int -> Int
getHP r = r hp

setName :: Robot (Robot a) -> String -> Robot a
setName r nm = r $ \(_, a, hp) -> robot (nm, a, hp)
setAttack :: Robot (Robot a) -> Int -> Robot a
setAttack r a = r $ \(nm, _, hp) -> robot (nm, a, hp)
setHP :: Robot (Robot a) -> Int -> Robot a
setHP r hp = r $ \(nm, a, _) -> robot (nm, a, hp)

printRobot :: Robot String -> String
printRobot r = r $ \(nm, a, hp) -> nm ++ " attack:" ++ show a ++ " hp:" ++ show hp

damage :: Robot (Robot a) -> Int -> Robot a
damage r ad = r $ \(nm, a, hp) -> robot (nm, a, hp - ad)

fight :: Robot Int -> Robot (Robot a) -> Robot a
fight atacker defender = damage defender power where
  power = if getHP atacker > 10
          then getAttack atacker
          else 0

Above, a Robot a indicates a robot value which can only be used to compute values of type a. E.g. from a Robot Int you can extract the attack and HP, but not the name.

Looking at the code... a lot of weird types arise! The type of fight is very puzzling:

fight :: Robot Int -> Robot (Robot a) -> Robot a

The first robot must produce its attack, so it is a Robot Int, while the second one must fight and produce a Robot a, hence the weird type Robot (Robot a).

From this, we get that we can't hope to type both fight r1 r2 and fight r2 r1: that would require Int = Robot a, which is impossible.

  • Couldn't match type ‘Int’ with ‘((String, Int, Int) -> a) -> a’
      Expected type: Robot (Robot a)
        Actual type: Robot Int

What could be the solution? Use rank-2 robots:

newtype Robot = Robot (forall a. ((String, Int, Int) -> a) -> a)

The forall a here indicates that a rank-2 robot can generate any result we choose, not just a single one. Hence, from a rank-2 robot we can extract both name and HP.

We need to wrap/unwrap everything using the constructor, which can be a bit annoying:

robot :: (String, Int, Int) -> Robot
robot (name, attack, hp) = Robot (\message -> message (name, attack, hp))
getName :: Robot -> String
getName (Robot r) = r name
-- etc.

Now, fight should work. I'll leave the rest for the OP to try.

Note that theoretical results (Yoneda's lemma) state that the polymorphic type we used, forall a. ((String, Int, Int) -> a) -> a is isomorphic to (String, Int, Int), so we indeed we have reinvented tuples in a more complicated way.

Concluding: I am a bit surprised that a Haskell book suggested this approach. It seems quite advanced material to me. I wonder what the intended solution was.

like image 134
chi Avatar answered Jul 03 '26 14:07

chi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!