7

I've just started to learn Haskell last night and I've never used a functional programming language before. I just want to know if my implemention of merge sort is good or bad and what exactly is good or bad. Maybe it's even wrong - Well it does sort but maybe the Algorithm is not what I think what merge sort is.

Just tell me everything I could improve here. I by myself think its a pretty clear and simple implementation. Thanks for your advice, here's the code :)

merge [] ys = ys
merge xs [] = xs
merge xs ys =  sorted : merge left right
                where 
                    sorted = if head(xs) < head(ys) then head(xs) else head(ys)
                    left = if head(xs) <= head(ys) then tail(xs) else xs
                    right = if head(xs) > head(ys) then tail(ys) else ys

msort [] = []
msort [x] = [x]
msort xs = merge (msort left) (msort right)
            where 
                left = take (div (length xs) 2) xs
                right = drop (div (length xs) 2) xs
Nocta
  • 199
  • 2
  • 9
  • 1
    in the two lines, `sorted =` and `left =`, you must use the same comparison; either `<` or `<=`, but both must be the same (and the third line, `right =`, will have to use the other variant accordingly). – Will Ness Nov 19 '12 at 16:12
  • Oh sorry, just saw that comment now. I guess you're right. If I understand it right, this little mistake only affects the stable/unstable attribute of the algorithm right? As it really doesn't matter if I take head(xs) or head(ys) if they are equal. – Nocta Nov 19 '12 at 18:20
  • No, of the two equals you'll drop one completely and have the other twice in your output: `merge [(1,1),(2,1)] [(1,2),(2,2)] = (1,2):merge [(2,1)] [(1,2),(2,2)]` for a pair-like datatype which compares on first sub-element only. – Will Ness Nov 19 '12 at 20:49

1 Answers1

14

Well, first of all, we can rewrite merge to be a little more elegant using pattern matching

merge [] ys = ys
merge xs [] = xs
merge xs@(x:xs1) ys@(y:ys1)
    | x <= y = x : merge xs1 ys
    | otherwise = y : merge xs ys1

In general you should avoid using head and tail since they are a bit unsafe (they raise an error for the empty list) and use pattern matching whenever possible.

The implementation of msort is pretty much spot on, except that we can split the list in a more efficient way. That's because length xs - takes O(N) to complete. The compiler might save you and cache the result of the length call so that the second call to length won't traverse the list again. But the take and drop will pretty much cause another two traversals thus splitting the list using 3 traversals which may prove to be expensive. We can do better by splitting the list in two lists - the first one containing the elements on the odd positions and the second list with the elements placed on the even positions, like so:

msort [] = []
msort [x] = [x]
msort xs = merge (msort first) (msort second)
    where
        (first, second) = splitInHalves xs
        splitInHalves [] = ([], [])
        splitInHalves [x] = ([x], [])
        splitInHalves (x:y:xs) =
            let (xs1, ys1) = splitInHalves xs
            in  (x:xs1, y:ys1)

This gets you the same Merge Sort in O(NlogN) time. It feels different because you would probably implement it in place (by modifying the original list) in an imperative language such as C. This version is slightly more costly on the memory, but it does have it's advantages - it is more easy to reason about, so it is more maintainable, and also it is very easy to parallelize without being concerned of anything else except the algorithm itself - which is exactly what a good programming language should provide for the developers that use it.

EDIT 1 :

If the syntax is a bit much, here are some resources:

  • Pattern Matching - the bit with the @ symbol is called an as-pattern. You'll find it in there
  • let is a keyword used to declare a variable to be used in the expression that follows it (whereas where binds a variable in the expression that precedes it). More on Haskell syntax, including guards (the things with | condition = value) can be found here, in this chapter of Learn You a Haskell

EDIT 2 :

@is7s proposed a far more concise version of splitInHalves using the foldr function:

splitInHalves = foldr (\x (l,r) -> (x:r,l)) ([],[])

EDIT 3 :

Here is another answer which provides an alternative implementation of merge sort, which also has the property of being stable:

Lazy Evaluation and Time Complexity

Hope this helps and welcome to the wonderful world of Functional Programming !

Community
  • 1
  • 1
Marius Danila
  • 10,311
  • 2
  • 34
  • 37
  • 5
    `splitInHalves = foldr (\x (l,r) -> (x:r,l)) ([],[])` – is7s Nov 19 '12 at 14:57
  • Hey, thank you very much for your precise feedback. I'm afraid I can't understand everything right now as I have not yet read about all the syntax ("let ... in ..." or "@") but I'll look these things up and try to understand your code :) – Nocta Nov 19 '12 at 14:58
  • @is7s - yep this is more concise, but for a newcomer in FP I think the more verbose version is better for starters. – Marius Danila Nov 19 '12 at 15:00
  • @Nocta - oh, no problem. I've added some links that explain the various constructs used. – Marius Danila Nov 19 '12 at 15:07
  • Thanks :) But couldn't you also achieve the same thing that @ does if you just write x:xs in the right side of the function? Meaning i could also avoid using head and tail without having an "ugly" @? What is the advantage here? – Nocta Nov 19 '12 at 15:20
  • Note: `splitInHalves` as implemented makes the sort unstable – singpolyma Nov 19 '12 at 15:22
  • @Nocta - the advantage is that it avoids creating the list again, since the (:) will allocate memory for another list node. But, yes, it's practically the same code. – Marius Danila Nov 19 '12 at 15:31
  • @singpolyma - it is indeed. Can you propose a solution which is still efficient and it is also stable ? – Marius Danila Nov 19 '12 at 15:35
  • 1
    The `splitInHalves [x, y] = ([x], [y])` case is superfluous, isn't it? – Landei Nov 19 '12 at 15:43
  • @M.A.D. stable mergesort: http://stackoverflow.com/questions/12057658/lazy-evaluation-and-time-complexity/12057921#12057921 . – Will Ness Nov 19 '12 at 15:55
  • @is7s is this a widely known piece of lore? I've seen it first here http://stackoverflow.com/questions/7942630/splitting-a-list-of-items-into-two-lists-of-odd-and-even-indexed-items/7945580#7945580 , in F#. Also, wouldn't it be better written with lazy pattern, `foldr (\x ~(l,r) -> (x:r,l)) ([],[])` as a matter of style? – Will Ness Nov 19 '12 at 15:58
  • @WillNess - thanks. this is quite an interesting implementation. I'll add a reference to it. – Marius Danila Nov 19 '12 at 16:02
  • @Landei - yes it is, I've made the fix. Thank you ! – Marius Danila Nov 19 '12 at 16:06
  • @M.A.D. that's a "bottom up" mergesort. (also, beware the rule of 10 edits: your answer will turn into pumpkin, er, community wiki post, and you won't collect any new rep on it). :) – Will Ness Nov 19 '12 at 16:06
  • Ah okay now it makes sense :) Still got some problems with foldr tough. I'm able to understand the result of this self-written example test = foldr (\x y -> 2*(x+y)) 5 [1, 2, 3] But what is missing here: One can't pass arguments to 'test'. How is this achieved in 'splitInHalves'? – Nocta Nov 19 '12 at 16:09
  • Another question that comes up into my mind when i read this stuff about stable and not stable: How can I pass data to msort that actually needs to be sorted in a stable way? Right now I know I can pass Strings, Lists of Strings, Lists of Ints, ... But none of these types has something like a "relative order" between elements that have the equal value in terms of the sort algorithm. In other words: How can I for example sort tuples by their first element and ignore the second element to actually create a case in which stable / unstable is a matter? – Nocta Nov 19 '12 at 16:17
  • @Nocta `foldr` is easy to visualize, as `foldr (+) 0 [a,b,c...z] = a + (b + (c + ... (z + 0) ...))` for some kind of generalized `(+)` and `0` (which must go together of course). IOW, when the combining function `(+) = \a b -> a + b` is called, `b` represents the *result* of calling `foldr` on the rest of the list. Check out the WP article http://en.wikipedia.org/wiki/Fold_(higher-order_function)#Folds_on_lists . – Will Ness Nov 19 '12 at 16:20
  • @Nocta - also look at http://www.haskell.org/haskellwiki/Partial_application to understand is7s's version – Marius Danila Nov 19 '12 at 16:20
  • @Nocta notice the `splitInHalves` is defined with one less argument. Because the type of `foldr` is known, the type of `splitInHalves` is automatically deduced (*"inferred"*) by a Haskell implementation as `[a] -> ([a], [a])`. Which means it is a function, expecting of a list (of *anything*, denoted by a type variable `a`), and producing a pair of lists (each holding elements of the same type as the input list). – Will Ness Nov 19 '12 at 16:26
  • @Nocta - well, as you can see the `msort`function works kinda magically for most of the types you throw at it. This is because we are using the operators provided by the Ord class: http://cvs.haskell.org/Hugs/pages/libraries/base/Data-Ord.html. This makes the code work for all the predefined types which support ordering and for your own types which can support ordering as well if you implement the `Ord` class. But we can modify `msort` so that it uses an arbitrary comparison function like in the link in EDIT 3. You can pass a function which compares tuples using the first element only. – Marius Danila Nov 19 '12 at 16:27
  • @Nocta about stability: if you sort compound data (say, pairs) and the comparison takes into account only one part of each element, e.g. (1,2) and (1,113) could be deemed equal. And in such a case, you might prefer to keep them in their original order. – Will Ness Nov 19 '12 at 16:30
  • Thanks for your patience guys, it's nice to learn new things that way. @Will Ness: Thanks but I understood the basic concept of foldr, what I am missing is how it "magically" takes an argument without defining it in the function like 'splitInHalves Argument = blabla' I also understand in which cases I would want a stable search but the question was more directed in the way: How does it work in Haskell? Well sorry, I guess my English is not that precise. – Nocta Nov 19 '12 at 16:43
  • @M. A. D.: Got to take some time to think about the splitInHalves thing and considering the facts you gave me. The msort thing kinda makes sense now even though hard to "fully understand" without some examples. But I'll leave it to that now and first try to understand the more basic things like the currying/partial application and how it affects the 'splitInHalves' function – Nocta Nov 19 '12 at 16:44
  • @Nocta do consult http://www.haskell.org/haskellwiki/Currying . Since `f x y z = code` is really `f = (\x y z -> code)` is really `f = (\x -> \y -> \z -> code)` is really `f = (\x -> (\y -> (\z -> code)))` is also `f x y = (\z -> code)` we can also write `f x y = newcode` and `f` will still be a function of 3 arguments, if `newcode` is a function of 1 argment. And `foldr g z` is a function of 1 argument: it expects a list. So `f = foldr g z` is a function of 1 argument. It expects a list. This is deduced from the type of `foldr` function. – Will Ness Nov 19 '12 at 16:55
  • @Nocta this is otherwise known as "eta-reduction": `f xs = code xs` can be reduced to `f = code`. See http://www.haskell.org/haskellwiki/Eta_conversion , http://en.wikipedia.org/wiki/Lambda_calculus#.CE.B7-conversion . – Will Ness Nov 19 '12 at 16:58
  • @Will Ness: Your first post makes a bit sense to me and I'm starting to get the idea. But maybe I need to go back a step in my learning process and come to that topic again when I have read more about Haskell :) Btw: Do I need advanced knowledge in subjects like "Logic" or can I understand the full concept of the language with a basic understanding of mathematics? – Nocta Nov 19 '12 at 17:20
  • @Nocta - I would say that for starters there is no need for logics and mathematics any more than there is for any other programming language. However, as you progress and discover Haskell's type system some simple first order logic may help you understand the notations and some of the concepts. Also, to understand at a more advanced level one of Haskell's most powerful concepts - the monads - you will need to get into some http://en.wikipedia.org/wiki/Category_theory. But you'll cross that bridge when you'll come to it :) – Marius Danila Nov 19 '12 at 17:41
  • Alright that sounds good :) Thank you so far guys you really helped me but I need a break for today :D – Nocta Nov 19 '12 at 17:54