0

it has been several days of trying to implement a functional recursive merge sort in Python. Aside from that, I want to be able to print out each step of the sorting algorithm. Is there any way to make this Python code run in a functional-paradigm way? This is what I have so far...

def merge_sort(arr, low, high):

    #  If low is less than high, proceed. Else, array is sorted
    if low < high:

        mid = (low + high) // 2             # Get the midpoint

        merge_sort (arr, low, mid)          # Recursively split the left half
        merge_sort (arr, mid+1, high)       # Recursively split the right half
        
        return merge(arr, low, mid, high)   # merge halves together

def merge (arr, low, mid, high):
    temp = []

    # Copy all the values into a temporary array for displaying
    for index, elem in enumerate(arr): 
        temp.append(elem)

    left_p, right_p, i = low, mid+1, low

    # While left and right pointer still have elements to read
    while left_p <= mid and right_p <= high:
        if temp[left_p] <= temp[right_p]:   # If the left value is less than the right value. Shift left pointer by 1 unit to the right
            arr[i] = temp[left_p]
            left_p += 1
        else:                               # Else, place the right value into target array. Shift right pointer by 1 unit to the right
            arr[i] = temp[right_p]
            right_p += 1

        i += 1                              # Increment target array pointer

    # Copy the rest of the left side of the array into the target array
    while left_p <= mid:
        arr[i] = temp[left_p]
        i += 1
        left_p += 1

    print(*arr) # Display the current form of the array

    return arr

def main():
    # Get input from user
    arr = [int(input()) for x in range(int(input("Input the number of elements: ")))]

    print("Sorting...")
    sorted_arr = merge_sort(arr.copy(), 0, len(arr)-1)      
    print("\nSorted Array")
    print(*sorted_arr)

if __name__ == "__main__":
    main()

Any help would be appreciated! Thank you.

sphynxo
  • 41
  • 2
  • 5

1 Answers1

0

In a purely functional merge sort, we don't want to mutate any values.

We can define a nice recursive version with zero mutation like so:

def merge(a1, a2):
  if len(a1) == 0:
    return a2
  if len(a2) == 0:
    return a1
  if a1[0] <= a2[0]:
    rec = merge(a1[1:], a2)
    return [a1[0]] + rec
  rec = merge(a1, a2[1:])
  return [a2[0]] + rec

def merge_sort(arr):
  if len(arr) <= 1:
    return arr
  halfway = len(arr) // 2
  left = merge_sort(arr[:halfway])
  right = merge_sort(arr[halfway:])
  return merge(left, right)

You can add a print(arr) to the top of merge_sort to print step-by-step, however technically side-effects will make it impure (although still referentially transparent, in this case). In python, however, you can't separate out the side effects from the pure computation using monads, so if you want to really avoid this print, you'll have to return the layers, and print them at the end :)

Also, this version is technically doing a lot of copies of the list, so it's relatively slow. This can be fixed by using a linked list, and consing / unconsing it. However that's out of scope.

414owen
  • 791
  • 3
  • 13
  • @rcgldr modifying the links in a linked list is mutation, as is modifying the values in the nodes. Copying the list and mutating the copy avoids mutation of the parameter to merge_sort, but this is still inherently writing an imperative program, which mutates state, even if your state is internal to your function. – 414owen Jan 21 '21 at 18:20
  • Creating a copy of the array and mutating it is not the same thing. At no point in my program do I mutate anything. It's a different algorithm and a different paradigm. I can see where you're coming from though, if we black box both versions, we get the same result :) The stack is an implementation detail of how the interpreter calculates the result of our function. If our code is pure, we should be able to translate it directly into case matching and beta reduction. With this code, we can. As a haskeller, this is how I would write it. If you want something less pure, that's fine too. – 414owen Jan 22 '21 at 01:39
  • The position of the program counter is also an implementation detail of how the interpreter /cpu evaluates your function. In a pure language, the runtime should be able to execute operations with no data dependencies in any order it wishes. Data dependency is the only guaranteed order of evaluation haskell provides. – 414owen Jan 22 '21 at 12:21
  • "each value of an array or list generally transitions through states via an ordered sequence of operations", you're going to have to be more specific here, I'm really not sure what you mean. If you need every ounce of performance, you can choose to use a copy-then-mutate sort in Haskell, too: https://hackage.haskell.org/package/vector-algorithms-0.8.0.4/docs/Data-Vector-Algorithms-Tim.html, the implementation is not functional at all, however mutation can only happen within the ST monad, and the ST monad doesn't allow other side effects, so there are still quite strong guarantees. – 414owen Jan 22 '21 at 23:25
  • In [APL](https://en.wikipedia.org/wiki/APL_(programming_language)), the wiki example with the values in `a[]`, could be done with `((~2|a)/a) +.× 10`, where ~ is not, | is modulo, / is reduction (removes elements on right corresponding to zeroes on the left), +.× is "inner product", that multiplies elements of `a[]` by 10 and sums them up, without specifying any order of operations (parallelization could be used). `a[]` is not modified, but the internal local variables are modified during the process. – rcgldr Jan 22 '21 at 23:39
  • The cpython implementation of `reduce` does indeed reassign a value on the stack ([source](https://github.com/python/cpython/blob/master/Lib/functools.py#L237-L263)). It does create `n` versions of the accumulator, the function passed to reduce creates one for every element in the list, the only reuse inherent to this implementation is the binding. A functional implementation of `reduce` would be [this](https://gist.github.com/414owen/2cac5b74ff271dd340658d755934b9e8). – 414owen Jan 23 '21 at 00:38
  • If I understand correctly, you're also talking about the implementation of reduce in APL being destructive? Yeah maybe your APL implmentation defines reduce in an imperative way. That doesn't make your program impure for using it. Every runtime has tonnes of mutable state (think registers, and garbage collection). Luckily, we're talking about a function that's defined as a pure expression, and its execution is still irrelevant. – 414owen Jan 23 '21 at 00:50
  • Monadic implies something very specific, I think the terms for operator arity you're looking for are 'unary' and 'binary' . – 414owen Jan 23 '21 at 11:59
  • APL uses the terms [monadic and dyadic](https://en.wikipedia.org/wiki/APL_syntax_and_symbols#Monadic_and_dyadic_functions). APL's reduce (reduction) operator is not destructive to a variable, it produces a new result. As with most languages, temporary internal variables are created, modified and deleted as needed. I'm still wondering if Haskell library functions are truly implemented as functional programming, or if similar to Python, most of the library is created from imperative, compiled modules.I deleted some prior comments since they are no longer needed. – rcgldr Jan 23 '21 at 18:15
  • I feel like I'm getting off topic. I should probably post my own question about functional programming and a sequence of if statements, rather than comment about this issue here. – rcgldr Jan 24 '21 at 11:27