Add the first two homework assignments.
This commit is contained in:
124
Hasklet2.md
Normal file
124
Hasklet2.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Phillip
|
||||
Hey man, long time no... read? Having seen your comment, I don't have anything exceptionally
|
||||
eye-opening to contribute, but here goes:
|
||||
|
||||
* At first glance, it seemed like it should be _easy_ to simplify all those 4-deep case statements
|
||||
into a single line, but I don't think that's quite the case. I think that if you were to just
|
||||
rewrite the `Maybe` code using Haskell's standard functions, you _would_ need to use `Maybe`'s
|
||||
`Monad` instance, even though I didn't need it for my `Applicative` parser data type. The difference
|
||||
is in the types. The result of a parser application is `Maybe (a, String)`, and that `String`
|
||||
argument is used by the next parser. `Applicative`, on the other hand, does not support
|
||||
making decisions based on the data inside the functor. The signatures for `fmap` and `<*>`
|
||||
are `(a -> b) -> f a -> f b` and `m (a -> b) -> m a -> mb`: you have to have both the function
|
||||
and its arguments _before_ you combine them.
|
||||
|
||||
On the other hand, when turning Parser into its own data type, the `String` state-passing can be
|
||||
hidden away, so instead of `Maybe (a, String)` you'll just have `Parser a`. At the type signature
|
||||
level, you no longer rely on the "state" (leftover string) in your combinators, so you only need
|
||||
`Applicative`.
|
||||
|
||||
In short, with `Maybe`, I think the best you can do is something like the following:
|
||||
|
||||
```Haskell
|
||||
do
|
||||
(_, s1) <- char '(' s
|
||||
(i, s2) <- nat s1
|
||||
...
|
||||
```
|
||||
Perhaps this reminds you of the [implementation of the State monad](https://wiki.haskell.org/State_Monad#Implementation)?
|
||||
My intuition is that a Parser is just a combination of the `State` and `Error` monads.
|
||||
|
||||
* I think that your implementation of `natPair` and `natTree` could be refactored a little bit.
|
||||
In particular, you can abstract the code for parsing "two things in parentheses separated by a comma",
|
||||
perhaps into a function like `pair :: Parser a -> Parser b -> Parser (a, b)`. If you did that,
|
||||
your 4-deep chain of case analysis would only occur in one place (in `pair`), and your other
|
||||
two functions would just call out to it. Applying just this refactoring step, you'd get:
|
||||
|
||||
```Haskell
|
||||
natPair = pair nat nat
|
||||
natTree s = case pair natTree natTree s of
|
||||
Just ((t1, t2), s') -> Just $ (Node t1 t2, s')
|
||||
Nothing -> case nat s of
|
||||
Just (n, s') -> Just $ (Leaf n, s')
|
||||
Nothing -> Nothing
|
||||
```
|
||||
|
||||
This has all the usual benefits of abstraction which I won't bore you with :-)
|
||||
|
||||
# Jack
|
||||
|
||||
Hey, sorry to see you didn't have time to finish up `natTree`. I've got a few comments:
|
||||
|
||||
* Your `(<|>)` implementation is actually nearly identical to `Maybe`'s implementation
|
||||
of `Alternative`'s `(<|>)`. In particular, you're effectively (lazily) combining two
|
||||
`Maybe` values, one from `p1` and one from `p2`. Thus, you can actually write that
|
||||
whole function as `p1 (<|>) p2 = \s -> p1 s (<|>) p2 s`. Well, except that
|
||||
then you have an ambiguous reference to `(<|>)`, so you have to qualify it,
|
||||
like `Control.Applicative.(<|>)`.
|
||||
|
||||
* You probably know this, but your helper functions `parseMap`, `ifTranP`, and `addP`
|
||||
are specializations of the standard functions `fmap`, `(>>=)`, and `liftA2`.
|
||||
In particular, `addP` is pretty much `liftA2 (:)`. This does, of course, rely
|
||||
on the `Functor`, `Monad`, and `Applicative` instances being defined
|
||||
for the `Parser` data type, which requires a bit of handywork given the starter
|
||||
code. The advantage, though, is getting access to all these fancy combinators
|
||||
from the standard library (like `*>` and `<*`). Similarly, your `\s -> Just ([], s)`
|
||||
could be written as `return []`.
|
||||
|
||||
* Our `nat` functions are practically identical! I went with pointfree style again
|
||||
(I have a bit of a problem, pointfree is not very readable at all), but other than
|
||||
that, it's scary how close our answers are!
|
||||
|
||||
* The whole "early return" pattern (check for `Just`, compute next `Maybe`, check for `Just` again)
|
||||
can at the very least be simplified as:
|
||||
|
||||
```Haskell
|
||||
natPair s1 = do
|
||||
(_, s2) <- char '(' s1
|
||||
(first, s3) <- nat s2
|
||||
(_, s4) <- char ',' s3
|
||||
(second, s5) -> case nat s4
|
||||
(_, s6) <- char ')' s5
|
||||
return $ ((first, second), s6)
|
||||
```
|
||||
|
||||
But wait a moment... we didn't actually do anything with the values of `first` and `second`!
|
||||
This means that we can generalize this function just a little bit (replace `nat` but an
|
||||
arbitrary input parser):
|
||||
|
||||
```Haskell
|
||||
pair p1 p2 s1 = do
|
||||
(_, s2) <- char '(' s1
|
||||
(first, s3) <- p1 s2
|
||||
(_, s4) <- char ',' s3
|
||||
(second, s5) -> case p2 s4
|
||||
(_, s6) <- char ')' s5
|
||||
return $ ((first, second), s6)
|
||||
```
|
||||
|
||||
Now, `natPair` can be written as `pair nat nat` (you can even verify this by some
|
||||
straightforward equational reasoning). And now that you have that, you can also
|
||||
define `natTree`. The first version:
|
||||
|
||||
```Haskell
|
||||
natTree = pair natTree natTree
|
||||
```
|
||||
|
||||
Alas, this is of type `Parser (Tree, Tree)`, not `Parser Tree`. To combine
|
||||
the two trees into one, we can use your `parseMap`:
|
||||
|
||||
```Haskell
|
||||
natTree = parseMap (uncurry Node) (pair natTree natTree)
|
||||
```
|
||||
|
||||
Oh, but we're missing a base case! We can use the `(<|>)` operator we defined earlier
|
||||
to define a "fallback" if we can't parse another level of the tree.
|
||||
|
||||
```Haskell
|
||||
natTree = parseMap (uncurry Node) (pair natTree natTree) <|> parseMap Leaf nat
|
||||
```
|
||||
|
||||
Two birds with one stone, right? Both `natPair` and `natTree` knocked out
|
||||
by a single `pair` function. It's true that defining `natPair` is quite
|
||||
messy, and hard to expand into `natTree`, but stuffing all that complexity
|
||||
into a helper function helps keep that messiness at bay :-)
|
||||
Reference in New Issue
Block a user