Homework/HW2.md

7.0 KiB

Syntactic Sugar Questions

Q: What are the benefits of extending a language using syntactic sugar vs. extending the abstract syntax?

A: Syntactic sugar can help separate concerns in the implementation of languages. For instance, rather than adding a feature such as let-expressions to a language's typechecker, one could conceivably do it using syntax sugar, and thus leave the typechecker completely unchanged. The Idris language has some nice examples of this. We can define "DSL blocks" which allow us to write expressions in our own DLS as if it were idris. This is mostly a matter of front-end: the core language (TT) is unaffected by this feature, which significantly eases implementation. However, if you want to add a core language feature, you probably need to modify the abstract syntax. For instance, if your abstract syntax doesn't support function application, you can't add it using syntax sugar, since syntax sugar relies on the core language to work. So, extending the abstract syntax can add more expressive power to your language, while syntax sugar can't.

Q: What are the drawbacks of extending a language using syntactic sugar vs. extending the abstract syntax?

A: For syntax sugar, you can make the evaluation model of the language more opaque to the user. For instance, our Expr language seems to perform addition as normal, when in fact it negates numbers first, then performs addition; a concrete 'negation' operation doesn't occur at runtime, but at compile time. This isn't that big of a deal, but can lead to some surprises. On the other hand, extending abstract syntax requires a lot of work because of the expression problem; pretty much anything that processes an Expr data type will need to be updated.

Submissions

Zuyu Cai's Solution

This solution is pretty straightforward, but it does provide unconventional (in my opinion) definitions of neg and sub. It performs the 'negation' transformation by first translating an expression into 'negated RPN', and then reading the RPN back in. An alternative solution would perform the negation operations directly on the tree (by pattern matching on it). I personally find that preferrable, since it avoids the use of the intermediate string representation of the expression. I think that sub can be improved; starting with the following code:

sub a b = fromRPN((toRPN(a) ++ " " ++ toNRPN(b) ++ " + "))

The parenthization in this example is in poor style; Haskell functions are typically applied via spaces, not parentheses. Rewriting:

sub a b = fromRPN (toRPN a ++ " " ++ toNRPN b ++ " + ")

The application of a function to a complicated argument is also a common use case for the ($) operator:

sub a b = fromRPN $ toRPN a ++ " " ++ toNRPN b ++ " + "

But even now, this function is a little strange. It uses the string representation of an expression, rather than its data type representation. What if we decide to change the syntax of our language? Also, there's some duplicated functionality here, in the call to toNRPN. We already have a neg function, so we can use it as follows:

sub a b = Add a (neg b)

I think this is the ideal way of solving this problem, but you can go even shorter (not necessarily simpler):

-- Weird because you take one argument explicitly, but one implicitly
sub a = Add a . neg

-- Weird because it's longer (and way more convoluted) than the original
sub = flip (.) neg . Add

Sina Jahedi's Solution

This is another straightforward solution, which would be very much like mine if I didn't play around with poin-free style. It uses foldl and a stack to convert RPN to Expr. I think there are tiny improvements you can make to neg and sub, though. For neg:

neg (Lit i) = (Lit (i - 2 * i))

The first argument of Lit is of type Int, and Int is an instance of the Num typeclass. What this means is that Int has a few functions from Num, one of which is negate. This function does exactly what you want: it returns the negative version of a number. So, you could write the above as:

neg (Lit i) = Lit (negate i)

For sub, I don't think you need the first case for Lit minus Lit. It works, and would result in smaller expressions, but the Add i (neg j) thing works just fine for all cases, too.

Andrey Kornilovich's Solution

First of all, this solution is amusing because it produces a much simpler implementation of neg than anyone else in our group! Everyone else, including myself, for some reason decided to traverse the tree recursively, negating certain literals to make the expression negative. I don't know why we did that! Multiplying by -1 is much simpler and not recursive. The only advantage of the recurive approach is that it doesn't add any nodes to the expression, but this is dubiously useful.

This solution is also straightforward, though it takes a different approach to fromRPN than me and the previous two solutions. It effectively fuses the head and fold code from the other examples into fromRPN'. This isn't better or worse - it's just different! There's a little trick you can do with the neg function as you have it:

neg x = Mul (Lit(-1)) x

Recall that the type for Mul is Expr -> Expr -> Expr, or, equivalently, Expr -> (Expr -> Expr). This means Mul is a function that takes an expression, and returns a function. This returned function also takes an expression, and returns an expression. What this means is that Mul e is the same as \x -> Mul e x: giving Mul only one argument makes it return a function that's 'waiting' for the other argument. So, neg is effectively taking in x, and feeding it straight into Mul (Lit (-1)). If you let f = Mul (Lit (-1)), you can see that:

neg x = f x

But then, all neg is doing is calling f! So we may as well write,

neg = f

We only used f for conciseness. Expanding:

neg = Mul (Lit (-1))

This trick is called eta conversion.

My Solution

My solution used the point-free style, and a custom fold' function for Expr. The fold function should be the most general function on Expr; that is, it should be possible to write any function of type Expr -> a using it. I do this with neg and toRPN. My solution to fromRPN is identical to two of the others, but my solution to sub is, perhaps, of some interest:

sub = fmap (. neg) Add

Even shorter, one can write that as:

sub = (. neg) <$> Add

This works because a function a -> b is a Functor; that is, there exists a function fmap :: (b -> c) -> (a -> b) -> a -> c. Now that I read the type, I think fmap = (.) for functions! So it could've been written even shorter as:

sub = (. neg) . Add

Basically, subtraction is almost like addition, but the second argument is negated using neg. So, I accept the first argument (producing Expr -> Expr, which is waiting for that second argument), and plop neg at the front of the result via the partially applied (.) operator: (. neg).