9

I'm trying to understand the monad system in Haskell. About 80% of my previous programming experiance is in C, but ironically the imperative part of Haskell is the hardest to understand. List manipulation and lazy evaluation was much more clear. Anyway I want to make ghc accept this code. I know the code doesn't make sense at all. Most obviously, I'm passing a Bool where IO Bool is expected. But that's not the only problem. I know this is a stupid question, but please help me to further my understanding of the Haskell language.

import Control.Monad

while :: Monad m => m Bool -> m () -> m ()
while cond action = do
  c <- cond
  when c $ do
    action
    while cond action

main :: IO ()
main = do
  i <- 0
  while (i < 10) $ do
    i <- i + 1
    print i

Here's how I finally did it. I know allocaArray isn't necessary, but it was very fun to use. Haskell really has no limits, very powerful.

import Control.Monad
import Data.IORef
import Foreign.Ptr
import Foreign.Storable
import Foreign.Marshal.Array

while :: Monad m => m Bool -> m () -> m ()
while cond action = do
  c <- cond
  if c then do
    action
    while cond action
  else return ()

main :: IO ()
main = do
  let n = 10
  allocaArray n $ \p -> do
    i <- newIORef 0
    while (liftM (< n) (readIORef i)) $ do
      i2 <- readIORef i
      poke (advancePtr p i2) i2
      modifyIORef i (+ 1)
    writeIORef i 0
    while (liftM (< n) (readIORef i)) $ do
      i2 <- readIORef i
      (peek $ advancePtr p i2) >>= print
      modifyIORef i (+ 1)
13
  • 3
    Let's start with the obvious. i is not a mutable variable, and doesn't become one simply because you have a monad around. i <- i + 1 refers to two different is. Commented Sep 10, 2015 at 14:28
  • 1
    The while construction is rather rarely used in Haskell, I think precisely because in Haskell it doesn't actually allow you to use "variables" in a natural way for people used to imperative languages. You can do the same thing more awkwardly, but you have to use mutable references such as those in Data.IORef or Control.Concurrent.MVar. Unless you really need mutable update, it's generally better to express it functionally. Commented Sep 10, 2015 at 14:31
  • 1
    Note that using an IORef in this manner leads to the loop counter being "boxed", so a new Int box will be allocated on each iteration and accessing the counter involves pointer indirection. When you deal with a more functional-style counter, GHC can usually unbox it, leading to faster code. Commented Sep 10, 2015 at 18:17
  • Also, why are you using all that Foreign stuff? Do you actually need to deal with foreign code? Do you have a reason to make the garbage collector treat the array as foreign? If not, you should consider using unboxed mutable vectors (from the vector package) instead. Commented Sep 10, 2015 at 18:22
  • There are also libraries that offer unboxed mutable variables; I don't know how well they perform. The basic idea is to use a mutable byte array to store a "variable". Commented Sep 10, 2015 at 18:28

3 Answers 3

11

The problem with this approach is that i is not a mutable variable. You could use IORef, however a more functional approach would be to pass the current state through each iteration. You could rewrite your whileM body and conditions to take the current value:

whileM :: Monad m => (a -> Bool) -> (a -> m a) -> a -> m ()
whileM test act init =
   when (test init) $ (act init) >>= whileM test act

then you can do

whileM (< 10) (\i -> print i >> return (i + 1)) 0
Sign up to request clarification or add additional context in comments.

Comments

6

A solution with local state (State and the associated monad transformer), as opposed to global state (IORef and friends):

import Control.Monad
import Control.Monad.State

while :: Monad m => m Bool -> m () -> m ()
while cond action = do
  c <- cond
  when c $ do
    action
    while cond action

main :: IO ()
main = do 
  runStateT (while cond body) 1 
  return ()

body :: StateT Integer IO ()
body = do
    x <- get
    liftIO $ print x
    put (x + 1)
    return ()

cond :: StateT Integer IO Bool
cond = do
    x <- get
    return (x < 10)

Loop body and loop condition are explicit and named for clarity; it's possible to write e.g. while (liftM (< 10) get) body.

Comments

5

There are two things which keep your code from typechecking:

  1. Your while function expects an IO Bool but you give it i < 10 which is an expression of type Bool. To turn a Bool into IO Bool, simply use return.

  2. When you write i <- 0 you try to use the literal zero as a monadic value, which it isn't. Remember that

    main = do
        i <- 0
        ...
    

    is equivalent to

    main = 0 >>= \i -> do ...
    

To fix this, you could also promote the 0 via return.

Hence, you end up with

main :: IO ()
main = do
    i <- return 0
    while (return (i < 10)) $ do
        i <- return (i + 1)
        print i

However, this still won't do what you intend to do: the reason is that the first (leftmost) i in i <- return (i + 1) is different than the i in i <- return 0. You're shadowing the variable, creating a new variable with the same name, which you then print. So you don't actually bump any counter at all.

I don't want to spoil the fun, but if you really get stuck: there's a monad-loops package which exposes a couple of useful monadic loop functions, including a whileM function.

2 Comments

The while in the OP is pretty much equivalent to that package's whileM_ (slightly less general type but not importantly); I don't think that's where the problem lies.
There are some other loopy libraries around, such as loops and control-monad-loop, that offer facilities better aligned with the OP's desires. monad-loops has some cool concurrency tools, but otherwise it seems less general.