Add discussion source for HW2.

This commit is contained in:
Danila Fedorin 2020-10-07 16:12:33 -07:00
parent 8a4f83ec37
commit 2a4d492810
1 changed files with 121 additions and 0 deletions

121
HW2.md Normal file
View File

@ -0,0 +1,121 @@
# 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:
```Haskell
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:
```Haskell
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:
```Haskell
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:
```Haskell
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):
```Haskell
-- 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`:
```Haskell
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:
```Haskell
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:
```Haskell
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:
```Haskell
neg x = f x
```
But then, all `neg` is doing is calling `f`! So we may as well write,
```Haskell
neg = f
```
We only used `f` for conciseness. Expanding:
```Haskell
neg = Mul (Lit (-1))
```
This trick is called [eta conversion](https://wiki.haskell.org/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:
```Haskell
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)`.