Let's start by cleaning up a little to make it easier what is going on.
The error is clearly within balanceRec
, so we don't even need to look at balance
:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
println(chars.mkString(", "))
if (chars.head == ')' &&
charStack.isEmpty
) {
false
}
else if (chars.head == ')' &&
charStack.pop() != '('
) {
false
}
else{
if(chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
Next, the println
clearly is not part of the problem either, so let's remove that:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
if (chars.head == ')' &&
charStack.isEmpty
) {
false
}
else if (chars.head == ')' &&
charStack.pop() != '('
) {
false
}
else{
if(chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
Further, let's remove some unnecessary parentheses and clean up the white space and indentation:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
Now the structure of the method becomes much clearer.
We notice now, that the first expression is a NO-OP: it has no side-effects, it doesn't mutate anything, it doesn't print anything, its result is not returned, its result is not printed, its result is not assigned to a variable or field. It literally does nothing at all.
That's the main problem.
As it turns out, all we need to do to fix the problem is insert one single else
keyword before the second if
:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
[Scastie link]
And since the body of our method is now a single expression, we can get rid of the curly braces:
def balanceRec(chars: List[Char]): Boolean =
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
Finally, some general tips.
You are not actually using any of the specific features of List
in your code, you are only using the more general features of Seq
, in fact, you are actually only using features of Iterable
. I prefer to keep my interfaces as generic and as general as possible, so I would change the type here to Iterable
:
def balance(chars: Iterable[Char]): Boolean = {
val charStack = new Stack[Char]()
def balanceRec(chars: Iterable[Char]): Boolean =
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
balanceRec(chars)
}
Next, I find the method awkward to use, because I have to pass in a collection of individual characters, when I would find it more natural to pass in a string:
def balance(s: String): Boolean = {
val charStack = new Stack[Char]()
def balanceRec(chars: Iterable[Char]): Boolean =
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
balanceRec(s.toIterable)
}
But actually, String
supports head
and tail
as well, so there is no need to use the Iterable
here:
def balance(s: String): Boolean = {
val charStack = new Stack[Char]()
def balanceRec(s: String): Boolean =
if (s.isEmpty) charStack.isEmpty
else if (s.head == ')' && charStack.isEmpty) false
else if (s.head == ')' && charStack.pop() != '(') false
else {
if (s.head == '(') charStack.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
I also prefer to use factory methods over constructors, so I would initialize the stack like this:
val charStack = Stack.empty[Char]
In functional programming, unlike many other communities, short variable names are perfectly acceptable, as long as the code still stays readable. Also, we generally trust our types, so for example, you don't have to say charStack
: we know it's a stack, because it has type Stack
! A typical naming pattern for naming a collection of things is to just append an s
to denote the plural, so we could denote this chars
or even just cs
(which, as a happy coincidence, could alternatively be read as an abbreviation of "char stack").
def balance(s: String): Boolean = {
val cs = Stack.empty[Char]
def balanceRec(s: String): Boolean =
if (s.isEmpty) cs.isEmpty
else if (s.head == ')' && cs.isEmpty) false
else if (s.head == ')' && cs.pop() != '(') false
else {
if (s.head == '(') cs.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
Now that we have shortened the code quite a bit, we notice that the second and third alternative (the first two else
s) have the same consequence: they both evaluate to false
. So, we can simply connect their conditions with an OR:
def balance(s: String): Boolean = {
val cs = Stack.empty[Char]
def balanceRec(s: String): Boolean =
if (s.isEmpty) cs.isEmpty
else if (s.head == ')' && cs.isEmpty || s.head == ')' && cs.pop() != '(') false
else {
if (s.head == '(') cs.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
Now we can apply some Boolean algebra and factor out the s.head =='('
condition:
def balance(s: String): Boolean = {
val cs = collection.mutable.Stack.empty[Char]
def balanceRec(s: String): Boolean =
if (s.isEmpty) cs.isEmpty
else if (s.head == ')' && (cs.isEmpty || cs.pop() != '(')) false
else {
if (s.head == '(') cs.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
Lastly, there is no need to have a mutable accumulator to pass information between different recursive calls, because the language already gives us tools that allow us to pass information into and out of calls: arguments and return values!
So, instead of keeping our state information in a mutable stack, we can simply pass an immutable stack (and we are simply going to use a List
for this) along as an argument and return it as a value:
def balance(s: String): Boolean = {
def balanceRec(s: String, cs: List[Char] = Nil): (Boolean, List[Char]) =
if (s.isEmpty) (cs.isEmpty, cs)
else if (s.head == ')' && cs.isEmpty || s.head == ')' && cs.head != '(') (false, cs)
else balanceRec(s.tail, if (s.head == '(') s.head :: cs else cs)
balanceRec(s)._1
}
Note that unlike what I said above, we are using List[Char]
here instead of Seq[Char]
, Iterable[Char]
, or String
, because in this case we do make use of one of the features of List
, namely O(1) prepend, head, and tail, which neither of the other three guarantee.
!!! Also note that this code is WRONG !!!
We did not account for the fact that your original code used side-effects, more precisely, mutation. For example, the second branch, even if it is not taken, may still lead to pop()
ping the top element of the stack. Let me repeat that: a branch that is not taken nevertheless changes the result.
In particular, if s.head == ')'
is true
and cs.isEmpty
is false
and cs.head != '('
is false
(in other words, cs.head == '('
is true
), then we pop()
the top-most element, and even though we are not taking this branch, this mutation is still visible in the later branches of the if
/ else
chain as well as future recursive calls.
So, we would have to account for that, in similar way as we accounted in the final else
:
def balance(s: String): Boolean = {
def balanceRec(s: String, cs: List[Char] = Nil): (Boolean, List[Char]) =
if (s.isEmpty) (cs.isEmpty, cs)
else if (s.head == ')' && cs.isEmpty) (false, cs)
else if (s.head == ')' && cs.head != '(') (false, cs.tail)
else if (s.head == ')' && cs.head == '(') balanceRec(s.tail, if (s.head == '(') s.head :: cs.tail else cs.tail)
else balanceRec(s.tail, if (s.head == '(') s.head :: cs else cs)
balanceRec(s)._1
}
Now, you might look at this, and say: "I thought functional programming is supposed to be so simple and so beautiful, but all I see here is that when we converted the mutable solution to an immutable one, it became a convoluted mess of triply-nested logic". But here's the thing: the mutable version had exactly that same mess! You were just not seeing it, because the dependencies between the different parts of the code that were connected by the mutable stack, were invisible. But they were still there.
As an alternative solution, note that we do not actually need to know the content of the stack at all. We only need to know how deep the stack is! So, we could instead do something like this:
def balance(s: String): Boolean = {
def balanceRec(cs: List[Char], depth: Int = 0): Int =
if (depth < 0) depth
else cs match {
case Nil => depth
case '(' :: cs => balanceRec(cs, depth + 1)
case ')' :: cs => balanceRec(cs, depth - 1)
case c :: cs => balanceRec(cs, depth)
}
balanceRec(s.toList) == 0
}