diff --git a/code/cs325-langs/sols/hw2.lang b/code/cs325-langs/sols/hw2.lang new file mode 100644 index 0000000..bbae5f9 --- /dev/null +++ b/code/cs325-langs/sols/hw2.lang @@ -0,0 +1,11 @@ +state 0; + +effect { + if(SOURCE == R) { + STATE = STATE + |LEFT|; + } +} + +combine { + STATE = STATE + LSTATE + RSTATE; +} diff --git a/content/blog/01_cs325_languages_hw2.md b/content/blog/01_cs325_languages_hw2.md new file mode 100644 index 0000000..ed36e6b --- /dev/null +++ b/content/blog/01_cs325_languages_hw2.md @@ -0,0 +1,153 @@ +--- +title: A Language for an Assignment - Homework 2 +date: 2019-12-30T20:05:10-08:00 +tags: ["Haskell", "Python", "Algorithms"] +--- + +After the madness of the +[language for homework 1]({{< relref "00_cs325_languages_hw1.md" >}}), +the solution to the second homework offers a moment of respite. +Let's get right into the problems, shall we? + +### Homework 2 +Besides some free-response questions, the homework contains +two problems. The first: + +{{< codelines "text" "cs325-langs/hws/hw2.txt" 29 34 >}} + +And the second: + +{{< codelines "text" "cs325-langs/hws/hw2.txt" 36 44 >}} + +At first glance, it's not obvious why these problems are good for +us. However, there's one key observation: __`num_inversions` can be implemented +using a slightly-modified `mergesort`__. The trick is to maintain a counter +of inversions in every recursive call to `mergesort`, updating +it every time we take an element from the +{{< sidenote "right" "right-note" "right list" >}} +If this nomeclature is not clear to you, recall that +mergesort divides a list into two smaller lists. The +"right list" refers to the second of the two, because +if you visualize the original list as a rectangle, and cut +it in half (vertically, down the middle), then the second list +(from the left) is on the right. +{{< /sidenote >}} while there are still elements in the +{{< sidenote "left" "left-note" "left list" >}} +Why this is the case is left as an exercise to the reader. +{{< /sidenote >}}. +When we return from the call, +we add up the number of inversions from running `num_inversions` +on the smaller lists, and the number of inversions that we counted +as I described. We then return both the total number +of inversions and the sorted list. + +So, we either perform the standard mergesort, or we perform mergesort +with additional steps added on. The additional steps can be divided into +three general categories: + +1. __Initialization__: We create / set some initial state. This state +doesn't depend on the lists or anything else. +2. __Effect__: Each time that an element is moved from one of the two smaller +lists into the output list, we may change the state in some way (create +an effect). +3. __Combination__: The final state, and the results of the two +sub-problem states, are combined into the output of the function. + +This is all very abstract. In the concrete case of inversions, +these steps are as follows: + +1. __Initializtion__: The initial state, which is just the counter, is set to 0. +2. __Effect__: Each time an element is moved, if it comes from the right list, +the number of inversions is updated. +3. __Combination__: We update the state, simply adding the left and right +inversion counts. + +We can make a language out of this! + +### A Language +Again, let's start by visualizing what the solution will look like. How about this: + +{{< rawblock "cs325-langs/sols/hw2.lang" >}} + +We divide the code into the same three steps that we described above. The first +section is the initial state. Since it doesn't depend on anything, we expect +it to be some kind of literal, like an integer. Next, we have the effect section, +which has access to variables such as "STATE" (to access the current state) +and "LEFT" (to access the left list), or "L" to access the "name" of the left list. +We use an `if`-statement to check if the origin of the element that was popped +(held in the "SOURCE" variable) is the right list (denoted by "R"). If it is, +we increment the counter (state) by the proper amount. In the combine step, we simply increment +the state by the counters from the left and right solutions, stored in "LSTATE" and "RSTATE". +That's it! + +#### Implementation +The implementation is not tricky at all. We don't need to use monads like we did last +time, and nor do we have to perform any fancy Python nested function declarations. + +To keep with the Python convention of lowercase variables, we'll translate the +uppercase "global" variables to lowercase. We'll do it like so: + +{{< codelines "Haskell" "cs325-langs/src/LanguageTwo.hs" 211 220 >}} + +Note that we translated "L" and "R" to integer literals. We'll indicate the source of +each element with an integer, since there's no real point to representing it with +a string or a variable. We'll need to be aware of this when we implement the actual, generic +mergesort code. Let's do that now: + +{{< codelines "Haskell" "cs325-langs/src/LanguageTwo.hs" 145 205 >}} + +This is probably the ugliest part of this assignment: we handwrote a Python +AST in Haskell that implements mergesort with our augmentations. Note that +this is a function, which takes a `Py.PyExpr` (the initial state expression), +and two lists of `Py.PyStmt`, which are the "effect" and "combination" code, +respectively. We simply splice them into our regular mergesort function. +The translation is otherwise pretty trivial, so there's no real reason +to show it here. + +### The Output +What's the output of our solution to `num_inversions`? Take a look for yourself: + +```Python +def prog(xs): + if len(xs)<2: + return (0, xs) + leng = len(xs)//2 + left = xs[:(leng)] + right = xs[(leng):] + (ls,left) = prog(left) + (rs,right) = prog(right) + left.reverse() + right.reverse() + state = 0 + source = 0 + total = [] + while (left!=[])and(right!=[]): + if left[-1]<=right[-1]: + total.append(left.pop()) + source = 1 + else: + total.append(right.pop()) + source = 2 + if source==2: + state = state+len(left) + state = state+ls+rs + left.reverse() + right.reverse() + return (state, total+left+right) +``` + +Honestly, that's pretty clean. As clean as `left.reverse()` to allow for \\(O(1)\\) pop is. +What's really clean, however, is the implementation of mergesort in our language. +It goes as follows: + +``` +state 0; +effect {} +combine {} +``` + +To implement mergesort in our language, which describes mergesort variants, all +we have to do is not specify any additional behavior. Cool, huh? + +That's the end of this post. If you liked this one (and the previous one!), +keep an eye out for more!