3

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.
9
  • 1
    Can you paste the errors you get? Commented Jan 24, 2018 at 15:55
  • Sure, I've just added them Commented Jan 24, 2018 at 16:03
  • The error messages do not match the code you posted (for example, the errors mention rb1' and rb2'', but they're nowhere in your code). Are you sure you have posted your actual code? Perhaps you have omitted some parts? Commented Jan 24, 2018 at 16:13
  • Source code is also included.. Commented Jan 24, 2018 at 16:22
  • 1
    I guess there's a problem with your damage function, but I haven't figured out why (it's making me mad without types :P). You can try this definition instead: damage aRobot attackDamage = robot (getName aRobot, getAttack aRobot, getHP aRobot - attackDamage) and it should work. Commented Jan 24, 2018 at 16:52

1 Answer 1

5

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.

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

2 Comments

Brilliant! It really helped. Still it doesn't explain the author's general idea of using this type of language constructions without any explanation. Initally a used that type synonyms: type RTuple = (String, Int, Int) type Robot a = (RTuple -> a) -> a, but they didn't help me because I didn't use this idea of {-# LANGUAGE RankNTypes #-}. Thnak you.
In my opinion, there's no advantage here. I have no idea about why the book suggests this. It could be a nice exercise for its own sake, since it forces you to use a lot of functions / continuations. But continuation passing style is known to make things much harder, and is only worth it when you need something like callCC or some other "early exit" mechanism. I would not consider suggesting this to a beginner.

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.