Scala: 99 problems
My colleague Liz Douglass and I have been playing around with Scala and Liz recently pointed out Phil Gold's 'Ninety Nine Scala Problems' which we've been working through.
One in particular which is quite interesting is number 7 where we need to flatten a nested list structure.
Therefore given this input:
flatten(List(List(1, 1), 2, List(3, List(5, 8))))
We would expect this output:
res0: List[Any] = List(1, 1, 2, 3, 5, 8)
I tried this out on my own using recursion but kept ending up creating a stack overflow by writing code that never terminated!
We decided to try it out while pairing together using a more imperative approach so that we could get something that actually worked to begin with and ended up with this:
def flatten[A](list : List[A]) : List[A] = { var flattenedList : List[A] = Nil for(item <- list) { item match { case x : List[A] => flattenedList = flatten(x) ::: flattenedList case _ => flattenedList = item :: flattenedList } } flattenedList }
It works and we've got a bit of recursion going on but we're still mutatating the state of a variable so it feels like we're missing out on the strength of a language which provides us with the ability to work in a functional way.
Our second attempt made use of the knowledge we gained from this first attempt with respect to that cases that existed for flattening the list and we made use of a technique which I picked up while learning a bit of F# – the inner function which keeps an accumulator with the flattened list on each recursion:
def flatten[A](list : List[A]) : List[A] = { def flat[A](rest : List[A], flattenedList : List[A]) : List[A] = rest match { case Nil => flattenedList case (head:List[A])::tail => flat(tail, flat(head, List()) ::: flattenedList) case head::tail => flat(tail, head :: flattenedList) } flat(list, Nil).reverse }
I like the fact that we don't have to mutate any state with this solution but the second case statement is pretty difficult to understand.
We had to distinguish between whether the head of the list was another list or just an element in order to determine whether we needed to break that value down further or just add it to the new list.
Is there a better way to do that than to explicitly state the type of head as we've done here?
Another colleague, Ben Barnard, came over to work with us on the next iteration of this solution which was somewhat driven by seeing how Phil Gold had solved the kth item problem for which he describes a solution which narrows down the initial list each time while still calling the same function:
def nth[T](n: Int, l: List[T]): T = (n, l) match { case (0, e :: _ ) => e case (n, _ :: tail) => nth(n - 1, tail) case (_, Nil ) => throw new NoSuchElementException }
In this case when we reduce the list we are searching on we also reduce the index that we are looking for by one which works quite nicely.
For the flatten function we were able to recurse on the original function by running the list back through the flatten function whenever we didn't know if the 'head' was an element:
def flatten[A](list : List[A]) : List[A] = list match { case Nil => list case (head:List[A])::tail => flatten(head) ::: flatten(tail) case head::tail => head :: flatten(tail) }
The nice thing about this solution is that the list is in the correct order at the end so we don't need to reverse it as we did with the previous solutions.
This way of coding is still not quite intuitive to me but I think the solution is cleaner and easier to read than the others.
Is there any point in the type parameter A, as it will nearly always be deduced as type Any?
Hector Chu
1 Oct 09 at 2:19 am
Hehe "not quite intuitive" that's the word,
"the solution is cleaner and easier to read" that's the message.
I never thought I would dig up my old Prolog assignments and fundamental scripts (#07) from https://prof.ti.bfh.ch/hew1/informatik3/prolog/p-99/
Thank you for keeping me hooked on Scala!
Oli
1 Oct 09 at 2:46 am
@Hector – yeh that's a good point. I think that's probably a hang over from the fact that in the previous problems 'A' could be a specific type but you're right for this problem it's irrelevant.
@Oli – looking at the Prolog problems reminds me of university days!
Mark Needham
1 Oct 09 at 6:07 am
FWIW the second option is tail recursive, so it can deal with indefinitely long lists, whereas the third option will stack overflow given a long enough input.
Raphael Speyer
1 Oct 09 at 8:59 am
I am still trying to wrap my head around functional programming in general. I find the below solution to make the most sense to me:
def flatten[A](list : List[A]) : List[A] = {
list.foldRight(Nil:List[A])((elem, start:List[A]) => {
elem match {
case x:List[A] => flatten(x) ::: start
case _ => elem :: start
}
}
)
}
Charlie Knudsen
1 Oct 09 at 12:02 pm
@Raphael – man I always spot the tail recursion but I missed this one! Good point.
@Charlie – that solution reads quite well. I think it's possible to do a solution using the built in function 'flatMap' which would make it even more concise.
Mark Needham
1 Oct 09 at 6:09 pm
[...] recently Liz and I have been playing around with Scala and from trying out some of these exercises I am seeing that when writing recursive functions my [...]
My Software Development journey: Year 3-4 at Mark Needham
5 Oct 09 at 6:54 pm