Conditional change of State in Haskell

543 views Asked by At

I don't know how I can make a conditional change to State Monad in Haskell. Suppose, I have a stack on State Monad.

import Control.Monad.State

push :: Int -> State [Int] ()
push x = state $ \xs -> ((), x : xs)

pop :: State [Int] Int
pop = state $ \(x : xs) -> (x, xs)

And with that, I want to write a function changing it.

someFunc :: Bool -> Int -> State [Int] ()
someFunc b x = do 
  let someValue = x * x
  if b then
    p <- pop
    someValue = someValue + p
  push $ someValue

For example, I want to take a bool b and a value x. And if b is True (for example, it could be a condition that my stack is not empty) I want to pop a value from my stack and add it to some variable. Otherwise, I don't add anything to the variable and just push it into the stack.

How can I achieve this kind of behaviour?

3

There are 3 answers

1
Aplet123 On BEST ANSWER

In Haskell, every if needs an else, since everything is just a value. Also, you can't do if ... then p <- pop ... since the do notation is lost in an if statement, so you need to restart it with if ... then do p <- pop ....

import Control.Monad.State

push :: a -> State [a] ()
push x = state $ \xs -> ((), x : xs)

pop :: State [a] a
pop = state $ \(x : xs) -> (x, xs)

someFunc :: (Num a) => Bool -> a -> State [a] ()
someFunc b x = do
  let baseValue = x * x
  someValue <- if b then do
        p <- pop
        return $ baseValue + p
      else do
        return baseValue
  push $ someValue
0
Will Ness On

Here's the smallest fix:

someFunc :: Bool -> Int -> State [Int] ()
someFunc b x = do 
  let someValue = x * x
  if b then
    do { p <- pop
       ; push $ someValue + p }
  else
    push $ someValue

Both branches of the if expression must have the same type, and inside the do notation that must be some monadic value type belonging to the same monad as does the overall do block; specifically the type of the overall do block if that if expression is the last one in it.

See also:

1
Ari Fordsham On

In addition to the question you asked, how to conditionally execute a monadic action without an else clause, there is another problem with your code. You want to reassign a let-variable inside a conditional clause.

This point has been addressed by the other answers, with correct versions of the code. I will elaborate here. All let-variables are immutable, including inside monads. You can redefine a variable in a monad, but that merely hides the original variable from code further down the same monadic action. The reassignment of someValue takes place inside a nested monadic action (there's a new do block) and is not visible outside of the if.

Conditionally reassigning a let-variable is fundamentally impossible. The compiler needs to know at compile time which value is being referred to. Ways to do this are either to return a value from the conditional and then assign the variable, like @Aplet123's answer, or run the action in both branches of the conditional on different values, as @Will Ness said.

In reply to the question as stated: you can conditionally run a monadic action (with no else branch) using the when function from Control.Monad. So if your code really had no else branch, you could write it like this:

someFunc :: Bool -> Int -> State [Int] ()
someFunc b x = do 
  let someValue = x * x
  when b $ do
    p <- pop
    push $ someValue + p

Of course this makes no sense for pure values, since a value must always be provided. But it does make sense for a monadic action (yielding (), or we have the same problem: what is the return value in case of a false conditional?) to be conditionally executed.

Just for curiousity, when is defined as follows:

when :: (Applicative f) => Bool -> f () -> f ()
when p s  = if p then s else pure ()

So pure () serves as the 'null' monadic action.

Happy Haskelling!