There are several candidate translations. Which you might choose depends on those constraints on the actual problem (instead of this pretend one) that make "f x = 0" not an eminently better solution.
The one that probably most closely matches the semantics of the pseudo-code would be:
let f0 x = unsafePerformIO $ do
iRef <- newIORef 17
whileM_ (fmap (>0) $ readIORef iRef) $ do
modifyIORef iRef (\ i -> i - 1)
readIORef iRef
Better would be to let Haskell help check that the mutation is local and no references escape:
let f1 x = runST $ do
iRef <- newSTRef 17
whileM_ (fmap (>0) $ readSTRef iRef) $ do
modifySTRef iRef (\ i -> i - 1)
readSTRef iRef
There's the more idiomatic translation, where for something this simple we wouldn't bother with State:
let f2 x = go 17
where
go i | i > 0 = go (i - 1)
| otherwise = i
The literal translation using State is:
let f3 x = execState countdown 17
where
countdown = whileM_ (>0) $ modify (\ i -> i - 1)
In all cases, the function as a whole is referentially transparent.
In your function, "while i > 0 { i := i - 1 }" is clearly not referentially transparent. Attempting to transliterate in a way that preserves this expression, it is unsurprising that we wind up with expressions that depend on state. The State monad is one way of encapsulating this. Note that f3 and f2 are actually very similar. The execState function doesn't do any deep voodoo - a value of type "State s a" is a thin wrapper around a function of type "s -> (a, s)"; execState simply extracts this function, applies it to a value, and returns the second part of the output.
The actual definitions are equivalent to these (albeit slightly more complicated to keep things DRY w/ transformers):
newtype State s a = State (s -> (a, s))
execState (State f) s = let (_, s') = f s in s'
What I don't see, in any of this, is a problem (conceptual or practical) involving "destructors". Is your objection to the need to call execState (or its friends)?
More precisely, since the argument isn't used it's type is unconstrained, and the result could be any numeric type, so they all actually have type (Num b => a -> b), but that can be used anywhere you're expecting an Int -> Int.
The one that probably most closely matches the semantics of the pseudo-code would be:
Better would be to let Haskell help check that the mutation is local and no references escape: There's the more idiomatic translation, where for something this simple we wouldn't bother with State: The literal translation using State is: In all cases, the function as a whole is referentially transparent.In your function, "while i > 0 { i := i - 1 }" is clearly not referentially transparent. Attempting to transliterate in a way that preserves this expression, it is unsurprising that we wind up with expressions that depend on state. The State monad is one way of encapsulating this. Note that f3 and f2 are actually very similar. The execState function doesn't do any deep voodoo - a value of type "State s a" is a thin wrapper around a function of type "s -> (a, s)"; execState simply extracts this function, applies it to a value, and returns the second part of the output.
The actual definitions are equivalent to these (albeit slightly more complicated to keep things DRY w/ transformers):
What I don't see, in any of this, is a problem (conceptual or practical) involving "destructors". Is your objection to the need to call execState (or its friends)?