4

I recently made a small algorithm to strip out function arguments from a snippet of code and only keep the outermost functions.
I found that this algorithm was very easy to design in an imperative way.
However, I'm really interested in functional programming and I was wondering how you would accomplish the same thing in a functional way.

It would be very helpful to me if you could show me how such an algorithm might work, so I might get a better idea of how functional programming works. Also I'd like to know what your thought process is while designing the algorithm.

I made the imperative version in Python, but your answer doesn't have to be in python; haskell or any other language will do just as well.

Here's what it does (taking a string as input and returning a string):

"foo(a.d, b.e.fi()).go(sd, ds())"     -- returns -->  "foo().go()"
"foo(a, b).bar().fuu"                 -- returns -->  "foo().bar().fuu"
"foo.bar"                             -- returns -->  "foo.bar"

And here's my imperative code:

def get_rid_of_arguments(text):
    i, start, end = 0, 0, 0
    result = ""
    for j, c in enumerate(text):
        if c == '(':
            if i == 0:
                start = j
                result += text[end:start]
            i += 1
        elif c == ')':
            i -= 1
            if i == 0:
                end = j + 1
                result += '()'
    return result + text[end:]
Zinggi
  • 43
  • 6
  • 1
    Here's something you might find helpful. It deals with a different, but relatively simple problem. http://neilmitchell.blogspot.ie/2013/09/repeated-word-detection-with-haskell.html – mhwombat Oct 04 '13 at 13:56
  • You want to do this for the actual function or just text representing the function? – DiegoNolan Oct 04 '13 at 14:48
  • @DiegoNolan Just on a string representing the function. – Zinggi Oct 04 '13 at 15:00

5 Answers5

7

Here's my version:

import Control.Monad
import Control.Monad.State

-- filter `str` with the "stateful" monadic predicate function `handleChar`, 
-- with an initial state of 0
getRidOfArguments :: String -> String
getRidOfArguments str = filterM handleChar str `evalState` 0

handleChar :: Char -> State Int Bool
handleChar '(' = modify (+1) >> gets (<= 1)
handleChar ')' = modify (max 0 . subtract 1) >> gets (== 0)
handleChar _   = gets (== 0)

My thought process was: we're filtering a list so filter comes to mind; however whether we keep or drop a character depends on some state (our count of open/closed parens). So the monadic filter function filterM is appropriate, and we can use the State monad to abstract that plumbing of our open/close count.

Let me know if you want more details on how the above works.

jberryman
  • 16,334
  • 5
  • 42
  • 83
  • Oh, so that's what a monad can be used for. Since I'm no haskell expert, I'd prefer if there was a simpler solution without a monad. Thanks for your answer. – Zinggi Oct 04 '13 at 16:03
  • 3
    Instead of that lambda in the second case might I suggets `(min 0 . subtract 1)` – daniel gratzer Oct 04 '13 at 16:03
  • I'm marking this as the accepted answer, since this seems to be the way to go. Also You've explained your thought process very well. However, I think Changaco's solution is easier to understand, but that might change when I get more familiar with monads. – Zinggi Oct 04 '13 at 17:35
  • @jozefg good idea, edited (although I think you mean `max` :)) – jberryman Oct 04 '13 at 19:35
  • @Zinggi yes, after considering I thought this more abstract, "fancy" solution would be best; it maybe shows how FP can make it clear precisely which parts of your solution are "stateful" or "effectful" (hand-wavy terms) and allow you to abstract those bits out. The state monad is difficult to understand at first (though not to use); some people have found this old tutorial I wrote helpful: http://brandon.si/code/the-state-monad-a-tutorial-for-the-confused/ – jberryman Oct 04 '13 at 21:00
  • @jberryman Thank you very much, but this is over my head right now. I'll have to do some more reading on basic monads first. – Zinggi Oct 04 '13 at 21:25
  • @Zinggi sure thing. A lot of people love [Learn you a haskell](http://learnyouahaskell.com/chapters) – jberryman Oct 04 '13 at 21:49
3

Alright, I'd prefer jberryman's solution, but if you'd like to avoid a monad, the try this

stateFilter :: (s -> a -> (s, Bool)) -> [a] -> s -> [a]
stateFilter f as state = snd $ foldr stepper (state, []) as
  where stepper (state, filtered) a =
          let (state', b) = f state a in
             if b then (state', a:filtered) else (state', filtered)

This keeps a state running through our filtering function and we just return whether the current value is true and our new state. Then your code is just

-- # Converted from jberrymans lovely answer
handleChar :: Int -> Char -> (Int, Bool)
handleChar s '(' = (max 0 (s - 1), s <= 1)
handleChar s ')' = (s +1, s <= 0)
handleChar s _   = (s, s == 0)

Now the state is explicit (and not as pretty) but perhaps easier to understand.

clean str = stateFilter handleChar str 0

Now this is nice and functional, the whole thing boils down to folding over the string. There's a bit of plumbing going on to track the state but once you start to grok Haskell a bit more this goes away nicely.

daniel gratzer
  • 52,833
  • 11
  • 94
  • 134
2

Plenty of answers already, but just to add to the list, here's one in very simplistic functional style.

It uses a helper function that takes a nesting count. So, 0 means not inside brackets, 1 means inside 1 pair etc. If n > 0 then we drop characters. If we hit a bracket increment/decrement n accordingly.

The helper function is basically a case-by-case description of that algorithm. If using it for real, you would dangle it off a "where" clause.

skipBrackets :: String -> String
skipBrackets s = skipper s 0

skipper :: String -> Int -> String

skipper [] _ = []
skipper ('(':xs) 0 = '(' : skipper xs 1
skipper (')':xs) 1 = ')' : skipper xs 0

skipper ('(':xs) n = skipper xs (n + 1)
skipper (')':xs) n = skipper xs (n - 1)

skipper (x:xs) 0 = x : skipper xs 0
skipper (x:xs) n = skipper xs n
Richard Huxton
  • 21,516
  • 3
  • 39
  • 51
  • +1, another good demonstration of converting from iterative to recursive style. Keeping track of the level isn't really needed in this case but it is useful in others. However when decrementing a level you should handle the possibility that it becomes negative, your code doesn't and thus fails very badly on mismatched parentheses. – Changaco Oct 05 '13 at 08:58
  • I really like this, this is the simplest solution so far imho, thank you. Actually I came up with the exact same solution too after reading all the answers ;) – Zinggi Oct 05 '13 at 09:16
  • 1
    @Zinggi, I only have two types of functional script - simple and not working. – Richard Huxton Oct 05 '13 at 09:42
1

One way to do it is to convert from iterative to recursive style. In other words, instead of using a for loop to execute some code multiple times, you achieve the same thing by making your function call itself.

An example in Haskell:

get_rid_of_arguments [] = []
get_rid_of_arguments ('(':xs) = "()" ++ (get_rid_of_arguments $ dropper xs)
get_rid_of_arguments (x:xs) = x : get_rid_of_arguments xs

dropper [] = []
dropper (')':xs) = xs
dropper ('(':xs) = dropper $ dropper xs
dropper (_:xs) = dropper xs

main = do
    print $ get_rid_of_arguments "foo(a.d, b.e.fi()).go(sd, ds())" == "foo().go()"
    print $ get_rid_of_arguments "foo(a, b).bar().fuu" == "foo().bar().fuu"
    print $ get_rid_of_arguments "foo.bar" == "foo.bar"

P.S. neither your original python code nor this Haskell code are correct ways to "strip out function arguments from a snippet of code", I'm just answering the "how do I translate this code" question.

Changaco
  • 790
  • 5
  • 12
  • I think this is very clear and easy to understand, well done! But what do you mean by it's not correct? – Zinggi Oct 04 '13 at 16:17
  • @Zinggi It's not correct because it doesn't handle all cases properly. For example neither function reports an error if the number of parentheses is odd. Another problem is that they drop everything between parentheses, even if it's not actually function arguments. Ideally you would use a real parser that produces an Abstract Syntax Tree you can operate on. – Changaco Oct 04 '13 at 16:32
  • You're right, thanks for pointing that out. In my case it was given that parenthesis are matched and I actually wanted to strip out everything between parenthesis. I think I haven't described the purpose of the algorithm properly. – Zinggi Oct 04 '13 at 16:38
0

One trick that I like when doing this sort of conversin is looking at tail calls as a sort of goto+variable assignment:

sumn n = addn n 0

addn i acc =
   if i == 0 then
      acc
   else
      addn (i-1) (acc + i)
def sumn(n):
  #lets pretend Python has gotos for now...
  i, acc = n, 0
 acc:
  if i == 0:
     return acc
  else:
     i, acc = (i-1), (acc + i)
     goto acc

In your case, this would translate kind of like

--Haskell pseudocode w/ string slicing
get_rid xs = go 0 0 0 0 ""
  where
    -- "go" is a common name for these tail-recursive loop functions.
    go i j start end result =
      if j < length xs then
         case xs !! j of
           '('  -> 
              if i == 0 then
                go (i+1) (j+1) j end (result ++ (text[end:start]))
              else
                go (i+1) (j+1) start end result
           ')' -> 
              if i == 1 then
                go (i-1) (j+1) start (j+1) (result ++ "()")
              else
                go (i-1) (j+1) start end result
           _ ->
              go i (j+1) start end result
      else
         result ++ text[end:]

The end result is super ugly because this is still a fundamentally imperative algorithm with lots of variable assignment going on. Additionally, the functional version makes it explicit that all your variables were in the largest scope available (the "go" loop): I guess you should be able to get rid of "start" and "end" by using an inner loop to find the next ")" instead of doing everything in the main loop (this is also valid for the original Python program).

There are also some minor style issues, like still using indexing and slicing on linked lists (its O(N) in Haskell instead of O(1)) and from using a tail recursive loop (gotos) instead of more structured folds (for loops). That said, the important point is that you can still write the original, imperative, version of the algorithm if you really want to.

hugomg
  • 68,213
  • 24
  • 160
  • 246
  • Well this looks more like c then an elegant functional solution ;) But you proved your point that you can translate the algorithm easily step by step. – Zinggi Oct 04 '13 at 16:43