3

I have the following code:

while :: IO Bool -> IO () -> IO ()
while test body =
  do b <- test
     if b
       then do {body ; while test body}  -- same-line syntax for do
       else return ()

I need to implement the factorial function using imperative-style programming. what I have to do is to create and initialize variables using newIORef, modify their values using a while loop with readIORef and writeIORef, then have the IO action return a pair consisting of the input n and the final result.

This is what I have done so far:

fact :: Integer -> IO (Integer, Integer)
fact n = do r <- newIORef n --initialize variable
            while
              (do {v <- readIORef n; n})
              (do {v <- readIORef r; writeIORef (...)) --modify the value (?)
            readIORef r

This is my attempt to write the factorial function. This is obviously does not work. Any help would be appreciated.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
user3055141
  • 109
  • 1
  • 2
  • 8
  • 5
    imperative really? ... wow you got a naughty teacher there ... anywyay I think it's easier for you if you add another *variable* - use two: an *accumulator* where you multiply up the result and a *index* that you count up to `n` (simulate a `for` loop with the `while`) – Random Dev Oct 21 '15 at 15:50
  • 1
    @Carsten Actually, I find this to be a nice exercise to check if someone knows how to use `IORef`s and understands `IO`. Sure, the produced code is expected to be hard to read, and quite unidiomatic. This is definitely not an exercise about elegance ;-) – chi Oct 21 '15 at 16:55
  • hey whatever helps you learn ... ;) – Random Dev Oct 21 '15 at 17:05

2 Answers2

3

I think maybe it's time to give you some working version:

fact :: Integer -> IO (Integer, Integer)
fact n = do
  i <- newIORef 1
  acc <- newIORef 1
  while (lessOrEqualN i) (step i acc)
  acc' <- readIORef acc
  return $ (n, acc')
  where
     lessOrEqualN iRef = do
       i' <- readIORef iRef
       return $ i' <= n
     step iRef accRef = do
       i' <- readIORef iRef
       acc' <- readIORef accRef
       writeIORef accRef (acc' * i')
       writeIORef iRef (i'+1)

as you can see I used an loop reference i and an accumulator reference acc always reading, writing the changing values.

To make this (hopefully) a bit more readable I extracted the test and the body of the while into lessOrEqualN and step.


Of course there are easier ways to do this (modifyIORef) but I guess you have to use those.


PS: you play with it a bit - maybe you want to handle negative values differently or whatever


this might be a bit cleaner (putting both mutables into the same ref):

fact :: Integer -> IO (Integer, Integer)
fact n = do
  ref <- newIORef (1,1)
  while (lessOrEqualN ref) (step ref)
  (_,acc) <- readIORef ref
  return $ (n, acc)
  where
     lessOrEqualN ref = do
       (i,_) <- readIORef ref
       return $ i <= n
     step ref = do
       (i,acc) <- readIORef ref
       writeIORef ref (i+1, acc * i)
Random Dev
  • 51,810
  • 9
  • 92
  • 119
  • And the answer is, of course, "basically the same as the usual version, but much more complicated for no reason". – Rein Henrichs Oct 21 '15 at 19:09
  • @ReinHenrichs ? right now I really have no clue what you are getting at - but I have the feeling I did something wrong !(?) – Random Dev Oct 21 '15 at 19:16
  • No, your solution seems fine (given the constraints), it's just that it's a ridiculous problem statement. I don't understand why someone would think that this is a good exercise. – Rein Henrichs Oct 21 '15 at 19:21
  • So this is imperative Haskell code... it makes me glad that I switched to Haskell. – AJF Oct 21 '15 at 19:48
  • I wasn't! I was just trying to point out that *any* answer would necessarily be convoluted because the question asks for a convoluted answer. It's basically "write a factorial function but add in a lot of indirection by using IORefs for no reason". – Rein Henrichs Oct 21 '15 at 20:23
  • I'm just kidding ... and I understand perfectly well ... I was just reacting to AJ's comment ... and I probably should just shut up - and so I will ;) – Random Dev Oct 21 '15 at 20:33
  • @ReinHenrichs I do think this _is_ a good exercise, if the point is learning how `IORef`s work -- even if the solution is horrible, especially when compared with pure ones. It's a matter of personal taste though. – chi Oct 21 '15 at 22:35
  • @chi It might use IORefs, but I don't think it motivates them at all, which for me is an essential part of an effective exercise. – Rein Henrichs Oct 21 '15 at 23:09
1

I think Carsten's answer can be made a bit cleaner like this:

{-# LANGUAGE TupleSections #-}

import Control.Monad
import Data.IORef

fact :: Integer -> IO (Integer, Integer)
fact n = do
  counter <- newIORef 1
  result <- newIORef 1
  while (fmap (<=n) (readIORef counter)) $ do
    i <- postIncrement counter
    modifyIORef result (*i)
  fmap (n,) (readIORef result)

while :: IO Bool -> IO () -> IO ()
while test body =
  do b <- test
     if b
       then do {body ; while test body}  -- same-line syntax for do
       else return ()

postIncrement :: Enum a => IORef a -> IO a
postIncrement ref = do
  result <- readIORef ref
  modifyIORef ref succ
  return result

What I'm doing here is:

  1. Using modifyIORef to cut down on the number of paired readIORef/writeIORef calls.
  2. Using fmap to reduce the need for auxiliary functions to test the contents of an IORef.
  3. Write a generic, reusable postIncrement function and use that to shorten fact further.

But frankly, I think your instructor's insistence that you use this while function is a bit silly. It doesn't make for clean code. If I was told to write an imperative factorial with IORef I'd first write this, just using the forM_ loop from the library:

factorial :: Integer -> IO (Integer, Integer)
factorial n = do
  result <- newIORef 1
  forM_ [2..n] $ \i -> do
    modifyIORef result (*i)
  fmap (n,) (readIORef result)

And that's because I was too dumb to remember replicateM_ :: Monad m => Int -> m a -> m () right away...

Luis Casillas
  • 29,802
  • 7
  • 49
  • 102