diff --git a/HW2.md b/HW2.md new file mode 100644 index 0000000..f911f90 --- /dev/null +++ b/HW2.md @@ -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)`.