Search code examples
haskellfunctional-programmingread-eval-print-loop

How can I make my function operating on lines interactive instead of waiting for the full input at once?


My interepreter supports the following operations:

  • up (increment the y coordinate),
  • down (decrement the y cooridinate),
  • left (decrement the x cooridinate),
  • right (increment the x coordinate),
  • printX (print the x coordinate),
  • printY (print the y coordinate).

My current implementation looks like this


interpreterHelper :: [String] -> Int -> Int -> Int -> [String] -> [String]
interpreterHelper cmds index x y output
  | index == length (cmds) = reverse output
  | (cmds !! index) == "up"     = interpreterHelper cmds (index + 1) x (y + 1) output
  | (cmds !! index) == "down"   = interpreterHelper cmds (index + 1) x (y - 1) output
  | (cmds !! index) == "left"   = interpreterHelper cmds (index + 1) (x - 1) y output
  | (cmds !! index) == "right"  = interpreterHelper cmds (index + 1) (x + 1) y output
  | (cmds !! index) == "printX" = interpreterHelper cmds (index + 1) x y (show x : output)
  | (cmds !! index) == "printY" = interpreterHelper cmds (index + 1) x y (show y : output) 

interpreter :: [String] -> [String]
interpreter cmds = interpreterHelper cmds 0 0 0 []

However, my exercise requires that I can make it interactive with the following statement:

interact (unlines . interpreter . lines)

which does not seem to happen.

How can I make it interactive, so that when I, say, type in printY, I immediately receive the output of the current value of the y - coordinate?


Solution

  • The obvious problem is that you collect all output into an accumulator first and only produce anything at the very end, so the code doesn't produce any output until the end.

    You can instead produce the output as you go:

    interpreterHelper :: [String] -> Int -> Int -> Int -> [String]
    interpreterHelper cmds index x y
      | (cmds !! index) == "up"     = interpreterHelper cmds (index + 1) x (y + 1)
      | (cmds !! index) == "down"   = interpreterHelper cmds (index + 1) x (y - 1)
      | (cmds !! index) == "left"   = interpreterHelper cmds (index + 1) (x - 1) y
      | (cmds !! index) == "right"  = interpreterHelper cmds (index + 1) (x + 1) y
      | (cmds !! index) == "printX" = show x : interpreterHelper cmds (index + 1) x y
      | (cmds !! index) == "printY" = show y : interpreterHelper cmds (index + 1) x y
    
    interpreter :: [String] -> [String]
    interpreter cmds = interpreterHelper cmds 0 0 0
    

    That does solve the immediate problem of interactiveness.
    But it eventually produces an error when we reach the end of the input (we can't use length because that would require all elements to be there again). And it's still fairly inefficient. Indexing into a list is often a habit of programmers coming from a non functional background, but for linked lists like Haskell's lists it's complexity is O(index) so your function is currently O(n²). You can improve that and in the process get rid of index by just matching on the cmds list:

    interpreterHelper :: [String] -> Int -> Int -> [String]
    interpreterHelper [] _ _ = []
    interpreterHelper ("up" : cmds) x y = interpreterHelper cmds x (y + 1)
    interpreterHelper ("printY" : cmds) x y = show y : interpreterHelper cmds x y
    -- other commands accordingly
    
    interpreter :: [String] -> [String]
    interpreter cmds = interpreterHelper cmds 0 0