When debugging my state monad, the order is canceled depending on where the trace is

So, I built the state Monad and faced with some problems with its lazy nature, which makes debugging difficult.

My state monad works by getting into the list of values, clicking them one after another on a part of the state, then I analyze what values ​​are in the state after each to create another part of the state.

I came up with this simple example to illustrate why it is difficult to debug.

module Main where import Control.Monad.State import Debug.Trace runSim :: [Int] -> State String String runSim [] = return =<< get runSim (cur:xs) = do lst <- get let val = (show $ 2*cur) put $ trace ((show $ length lst) ++ " " ++ (show cur)) ((val) ++ "," ++ lst) runSim xs main :: IO () main = print $ evalState (runSim [1..10]) "" 

The result of this:

 0 1 2 2 4 3 6 4 8 5 11 6 14 7 17 8 20 9 23 10 "20,18,16,14,12,10,8,6,4,2," 

However, if I change my trace string to this:

 put $ trace ((show cur)) ((val) ++ "," ++ lst) 

Reverse Output:

 10 9 8 7 6 5 4 3 2 1 "20,18,16,14,12,10,8,6,4,2," 

But the end result is the same. Is there a better way to deal with the laziness of the state monad when debugging, so is it more naturally consistent?

+4
source share
2 answers

The problem is that trace calls are evaluated only at the end.

The calculation will build the type state (for brevity, only two elements are used for the list)

 runSim [1, 2] "" ~> ( (), state1@ (trace (output 1 "") (logString 1 ""))) ~> runSim [2] ( (), trace (output 2 state1) (logString2 state1)) 

therefore, in the final state, trace for the last element of the draggable list is the most external.

Now in the second case, where

 output i _ = show i 

trace output does not depend on what happened earlier, so trace push last is started first, etc.

But in the first case, where

 output i state = show (length state) ++ " " ++ show i 

trace output depends on the state, so the state must be evaluated before trace output. But state is a call to the previously pressed trace , so tracing should be done first, etc. And therefore, the data dependencies in the trace output ensure that the traces will be executed in the order of clicking.

To ensure that traces run in this order without data dependencies, you must either call trace from put or force the put state to be evaluated.

 put $! trace ((show $ length lst) ++ " " ++ (show cur)) ((val) ++ "," ++ lst) 

or

 trace ((show $ length lst) ++ " " ++ (show cur)) $ put ((val) ++ "," ++ lst) 
+8
source
  • This is the monad law, which m >>= return === m . Therefore, you can leave return =<< in runSim [] .
  • The end result is wrong because you are adding the current value to the state, but what you want is adding. Change ((val) ++ "," ++ lst) to (lst ++ "," ++ val) , and your result is what you expected: ",2,4,6,8,10,12,14,16,18,20" . (You may worry about the leading comma later.)
  • The state monad is intended for reading and writing states. In your case, all you want to do is write, and for the Monad Writer for:

     import Control.Monad.Writer import Data.List (intercalate) main = putStrLn . intercalate ", " . map show $ execWriter (runSim [1..10]) runSim :: [Int] -> Writer [Int] () runSim [] = return () runSim (x:xs) = tell [2*x] >> runSim xs ==> "2, 4, 6, 8, 10, 12, 14, 16, 18, 20" 

    (Note that Writer [] can be very inefficient when the written list is larger. Use DList if you have written a lot.)

  • General note. Since Haskell consists mainly of small functions that work well together, it is often very easy to talk about what a single function does. Your runSim gets the current state and computes a double header list. After that, it performs some trace functions, and then returns val ++ "," ++ lst as a new state before recursing as the last step. The great thing about Haskell is that the only thing this function can do is take state, multiply, put state. If you understand one iteration of this, you will understand all iterations. I want to say that even in your case, this is not a problem with laziness - laziness does not affect the result of your program (and really cannot, if you have reasons there). It doesn't matter that trace gives you an intermediate step, if the GHC decides to look at the list with even numbers first and then forward it with the odd ones, the result will still be the same. trace however contains evil magic, so this can be misleading. I would use trace more as a way to open up in your program, rather than speculate about how it does what it does.
0
source

Source: https://habr.com/ru/post/1442818/


All Articles