Add the first two homework assignments.
This commit is contained in:
commit
4e7d49fe0d
143
Hasklet1.hs
Normal file
143
Hasklet1.hs
Normal file
|
@ -0,0 +1,143 @@
|
|||
module Hasklet1 where
|
||||
|
||||
|
||||
-- | A generic binary tree with values at internal nodes.
|
||||
data Tree a = Node a (Tree a) (Tree a)
|
||||
| Leaf
|
||||
deriving (Eq,Show)
|
||||
|
||||
|
||||
-- | Build a balanced binary tree from a list of values.
|
||||
tree :: [a] -> Tree a
|
||||
tree [] = Leaf
|
||||
tree (x:xs) = Node x (tree l) (tree r)
|
||||
where (l,r) = splitAt (length xs `div` 2) xs
|
||||
|
||||
|
||||
-- Some example trees containing integers.
|
||||
t1, t2, t3, t4 :: Tree Int
|
||||
t1 = Node 1 Leaf (Node 2 Leaf Leaf)
|
||||
t2 = Node 3 (Node 4 Leaf Leaf) Leaf
|
||||
t3 = Node 5 t1 t2
|
||||
t4 = tree (filter odd [1..100])
|
||||
|
||||
treeFold :: (a -> b -> b -> b) -> b -> Tree a -> b
|
||||
treeFold _ b Leaf = b
|
||||
treeFold f b (Node a t1 t2) = f a (treeFold f b t1) (treeFold f b t2)
|
||||
|
||||
-- An example tree containing a secret message!
|
||||
t5 :: Tree Char
|
||||
t5 = tree " bstyoouu rd oerrvialentikne"
|
||||
|
||||
|
||||
-- | Define a recursive function that sums the numbers in a tree.
|
||||
--
|
||||
-- >>> sumTree Leaf
|
||||
-- 0
|
||||
--
|
||||
-- >>> sumTree t3
|
||||
-- 15
|
||||
--
|
||||
-- >>> sumTree t4
|
||||
-- 2500
|
||||
--
|
||||
sumTree :: Num a => Tree a -> a
|
||||
sumTree Leaf = 0
|
||||
sumTree (Node a t1 t2) = a + sumTree t1 + sumTree t2
|
||||
|
||||
|
||||
-- | Define a recursive function that checks whether a given element is
|
||||
-- contained in a tree.
|
||||
--
|
||||
-- >>> contains 57 t4
|
||||
-- True
|
||||
--
|
||||
-- >>> contains 58 t4
|
||||
-- False
|
||||
--
|
||||
-- >>> contains 'k' t5
|
||||
-- True
|
||||
--
|
||||
-- >>> contains 'z' t5
|
||||
-- False
|
||||
--
|
||||
contains :: Eq a => a -> Tree a -> Bool
|
||||
contains _ Leaf = False
|
||||
contains v (Node a t1 t2) = v == a || contains v t1 || contains v t2
|
||||
|
||||
|
||||
-- | Define a function for converting a binary tree of type 'Tree a' into
|
||||
-- a value of type 'b' by folding an accumulator function over the tree.
|
||||
-- You should start by writing a type definition for the function.
|
||||
--
|
||||
-- Note there is more than one correct type for this function! Part of your
|
||||
-- task is to figure out the type. For inspiration, think about the types of
|
||||
-- the functions `foldl` and `foldr` for lists.
|
||||
--
|
||||
foldTree :: (a -> b -> b -> b) -> b -> Tree a -> b
|
||||
foldTree _ b Leaf = b
|
||||
foldTree f b (Node a t1 t2) = f a (foldTree f b t1) (foldTree f b t2)
|
||||
|
||||
|
||||
-- | Use 'foldTree' to define a new version of 'sumTree'.
|
||||
--
|
||||
-- >>> sumTreeFold Leaf
|
||||
-- 0
|
||||
--
|
||||
-- >>> sumTreeFold t3
|
||||
-- 15
|
||||
--
|
||||
-- >>> sumTreeFold t4
|
||||
-- 2500
|
||||
--
|
||||
sumTreeFold :: Num a => Tree a -> a
|
||||
sumTreeFold = foldTree ((.(+)).(.).(+)) 0
|
||||
|
||||
|
||||
-- | Use 'foldTree' to define a new version of 'contains'.
|
||||
--
|
||||
-- >>> containsFold 57 t4
|
||||
-- True
|
||||
--
|
||||
-- >>> containsFold 58 t4
|
||||
-- False
|
||||
--
|
||||
-- >>> containsFold 'v' t5
|
||||
-- True
|
||||
--
|
||||
-- >>> containsFold 'q' t5
|
||||
-- False
|
||||
--
|
||||
containsFold :: Eq a => a -> Tree a -> Bool
|
||||
containsFold v = foldTree (\a b c -> a == v || b || c) False
|
||||
|
||||
|
||||
-- | Implement a function that returns a list of values contained at each
|
||||
-- level of the tree. That is, it should return a nested list where the
|
||||
-- first list contains the value at the root, the second list contains the
|
||||
-- values at its children, the third list contains the values at the next
|
||||
-- level down the tree, and so on.
|
||||
--
|
||||
-- Apply this function to 't5' to reveal the secret message!
|
||||
--
|
||||
-- >>> levels Leaf
|
||||
-- []
|
||||
--
|
||||
-- >>> levels t1
|
||||
-- [[1],[2]]
|
||||
--
|
||||
-- >>> levels t2
|
||||
-- [[3],[4]]
|
||||
--
|
||||
-- >>> levels t3
|
||||
-- [[5],[1,3],[2,4]]
|
||||
--
|
||||
-- >>> levels (tree [1..10])
|
||||
-- [[1],[2,6],[3,4,7,9],[5,8,10]]
|
||||
--
|
||||
levels :: Tree a -> [[a]]
|
||||
levels = foldTree (\a b c -> [a] : padded b c) []
|
||||
where
|
||||
padded [] xs = xs
|
||||
padded xs [] = xs
|
||||
padded (x:xs) (y:ys) = (x ++ y) : padded xs ys
|
144
Hasklet1.md
Normal file
144
Hasklet1.md
Normal file
|
@ -0,0 +1,144 @@
|
|||
# Ben
|
||||
I was surprised to see how different our solutions were! Usually for "day 1" exercises,
|
||||
most answers come out pretty similar, especially for people who feel pretty comfortable
|
||||
with Haskell. But hey, more things to talk about!
|
||||
|
||||
* In your `sumTree`, you used a `foldr`. To me, this is kind of weird -
|
||||
I see your "or ..." comment, and I much prefer the version there which
|
||||
uses a simple summation. Setting aside whatever magic optimiztions
|
||||
GHC has in store for us, the version you have uncommneted
|
||||
will create an intermediate list, and possibly also
|
||||
an unevaluated "thunk" of the `foldr` application
|
||||
(instead of just adding numbers). It seems like a lot of work,
|
||||
and is, in my opinion, _less_ expresive than the "simple" version.
|
||||
* In your `containsTree`, you have the following: `| x == y = True`.
|
||||
This is reminiscent of a C-style `x ? true : false`. I would say this
|
||||
is an antipattern - returning true of something is the case, and
|
||||
trying another condition of it's not, is exactly the way that a short-circuiting
|
||||
`(||)` operator behaves. I think a simple `||` would suffice.
|
||||
* You defined a function `cx` for `contains x`. This is quite cool: it helps
|
||||
save on a lot of repetition! In this case, I think it's less valuable:
|
||||
there's a maxim that I heard, "if you need to write something twice,
|
||||
cringe and write it twice. If you need to write something more than that,
|
||||
abstract it". In this case, I think the `cx` abstraction is not worth the effort.
|
||||
Haskell's effortless creation of closures is pretty cool, though: suppose
|
||||
that it was the _leaves_ that contained data (such an example would be more convincing):
|
||||
|
||||
```Haskell
|
||||
data Tree a = Leaf a | Node (Tree a) (Tree a)
|
||||
```
|
||||
|
||||
You could then define a `containsTree` function like this:
|
||||
|
||||
```Haskell
|
||||
containsTree :: Eq a => a -> Tree a -> Bool
|
||||
containsTree a = ct
|
||||
where
|
||||
ct (Leaf x) = a == x
|
||||
ct (Node l r) = ct l || ct r
|
||||
```
|
||||
|
||||
Note that here we no longer need to pass around the `a` in recursive calls.
|
||||
This would become especially good if `Tree` had more cases (which would all have recursive
|
||||
calls). We used this in `Xtra` to implement the evaluation function for expressions -
|
||||
instead of passing around the environment `env`, we captured it like we captured `a` in the above example.
|
||||
* You defined your `foldTree` differently from the way I did it. As Eric said, there are multiple
|
||||
approaches to doing this, so I wouldn't say either of us is wrong. Tradeoff wise, your solution
|
||||
imposes an order on the elements of the tree: in effect, it converts them to a flat list:
|
||||
you can _really_ see this if you do `foldTree (flip (:)) []`. This makes it easy to express
|
||||
sequential computations, like for instance those for `sum` and `contains`. In fact, you can
|
||||
even re-use list-based functions like so:
|
||||
```Haskell
|
||||
toList = foldTree (flip (:)) []
|
||||
sumTree = sum . toList
|
||||
containsTree a = contains a . toList
|
||||
```
|
||||
In short, your approach makes it really easy to express some computations. However, unlike
|
||||
`fold` for lists, you cannot use `foldTree` to define any function on trees. Consider
|
||||
the simple example of `depth`, and two trees:
|
||||
* `Node 1 (Node 2 (Node 3 Leaf Leaf) Leaf) Leaf`
|
||||
* `Node 1 (Node 2 Leaf Leaf) (Node 3 Leaf Leaf)`
|
||||
|
||||
If you run them through `toList`, you'll notice that they produce the same result. Your
|
||||
`b -> a -> b` function is seeing the exact same order of inputs. However, the trees obviously
|
||||
have different depth: the first one has depth 2, and the second has depth 3.
|
||||
|
||||
My approach is different: I aimed to define the most general function on trees. I think that
|
||||
this is called a catamorphism. Were you there on the day we read the _Bananes, Lenses and Barbed
|
||||
Wire_ paper in reading group? It's like that. This ends up with a different signature than
|
||||
the `fold` for lists, but it makes it possible to define _any_ function for lists. For example,
|
||||
here's that depth function I mentioned earlier:
|
||||
```Haskell
|
||||
depth = foldTree (\_ l r -> 1 + max l r) 1
|
||||
```
|
||||
And, of course, my `levels` function is also implemented using `foldTree`, though
|
||||
I did need to define an auxillary function for zipping lists. This has the downside
|
||||
of making some "linear" code (like summations and "contains") look a little
|
||||
uglier. My function parameters were `(\a b c -> a + b + c)` and `(\a b c -> a == n || b || c)`
|
||||
for sum and contains respectively, and that's a little less pretty than, say, `(+)`.
|
||||
Don't mind that I wrote my `a + b + c` function as `((.(+)).(.).(+))`: I was
|
||||
just playing around with point-free style.
|
||||
|
||||
Interestingly, if you recall Church encoding from CS 581, you will notice that
|
||||
the "type" of a Church encoded list is `(a -> b -> b) -> b -> b`, and
|
||||
the type of a Church encoded tree as ours is `(a -> b -> b -> b) -> b -> b`.
|
||||
There's a connection between the representation of our data structure and
|
||||
the most general function on that data structure.
|
||||
* I didn't think of your approach to `levels`! I have a question about it,
|
||||
though: For a complete tree of depth `n`, doesn't your approach perform
|
||||
`n` traversals of the tree, once for each depth? This would mean you
|
||||
check `1 + (1 + 2) + (1 + 2 + 4) + ...` nodes while running this function,
|
||||
doesn't it?
|
||||
|
||||
# Ashish
|
||||
Hey there! I've got some case-by-case thoughts about your submission.
|
||||
|
||||
* In your `containsTree`, you write `if (n == m) then True else ...`. As I mentioned
|
||||
to Ben, this is very similar to writing `n == m ? true : false` in C/C++: I'd
|
||||
say it's a bit of an antipattern. Specifically, the short-circuiting `||` operator
|
||||
will do exactly that; you can write `n == n || ...`, instead.
|
||||
* There's a slight issue with your `foldTree` function, which is what caused
|
||||
to have trouble with `containsFold`. Take a look at your signature:
|
||||
```Haskell
|
||||
foldTree :: (a -> a -> a) -> a -> Tree a -> a
|
||||
```
|
||||
Note the very last part: `Tree a -> a`. This means that you can only
|
||||
use your `treeFold` function to produce _the same type of values that
|
||||
are in the tree_! This works for `sumTreeFold`, because numbers are closed
|
||||
under addition; it doesn't, however, work for `containsTreeFold`, since
|
||||
even if your tree contains numbers, you'd need to produce a boolean,
|
||||
which is a different type! The simple solution is to introduce a type variable `b`
|
||||
alongside `a`. This is strictly more general than using only `a` everywhere:
|
||||
`b` can be equal to `a` (much like `x` and `y` can be equal in an equation),
|
||||
but it can also be different. Thus, your `sumTreeFold` would still work,
|
||||
but you'd be able to write `containsTreeFold` as well. I think Ben's
|
||||
solution is exactly what you were going for, so it doesn't make sense for
|
||||
me to re-derive it here.
|
||||
* I'm really having trouble understanding your attempted solution for `levels`.
|
||||
If you're strill trying to figure it out, here's how I'd do it.
|
||||
|
||||
* For a leaf, there are no levels, so your solution would just be `[]`.
|
||||
* For a node in the form `Node n lt rt`, your solution would
|
||||
have the form `[n] : lowerLevels`. But how do you get `lowerLevels`?
|
||||
Suppose that `lt` has the levels `[1,2], [3,4,5,6]` and `rt` has the levels
|
||||
`[7, 8], [9, 10, 11, 12]`. You want to combine each corresponding level:
|
||||
`[1,2]` with `[7,8]`, and `[3,4,5,6]` with `[9,10,11,12]`. This is
|
||||
_almost_ like the function `zipWith` from the standard library in Haskell;
|
||||
However, the problem is that `zipWith` stops recursing when the shorter list
|
||||
runs out. We don't want that: even if the left side of the tree has no more levels,
|
||||
if the right side does, we want to keep them. Thus, we define the following function:
|
||||
```Haskell
|
||||
myZipWith :: [[a]] -> [[a]] -> [[a]]
|
||||
myZipWith [] [] = [] -- two empty lists means we've run out of levels on both ends, so we're done.
|
||||
myZipWith (l:ls) (m:ms) = (l ++ m) : myZipWith ls ms -- Combine the first levels from both lists, and recurse.
|
||||
myZipWith [] ls = ls -- We ran out of levels on the left, so only the right levels occur from here on.
|
||||
myZipWith ls [] = ls -- We ran out of levels on the right, so only the left levels occur from here on.
|
||||
```
|
||||
Our final function implementation is then:
|
||||
```Haskell
|
||||
levels Leaf = []
|
||||
levels (Node m lt rt) = [m] : lowerLevels
|
||||
where lowerLevels = myZipWith lt rt
|
||||
```
|
||||
I implemented mine using my custom `fold`, but in essense it works the same way. My `myZipWith` is
|
||||
called `padded`, but the implementation is identical to what I showed here.
|
177
Hasklet2.hs
Normal file
177
Hasklet2.hs
Normal file
|
@ -0,0 +1,177 @@
|
|||
{-# LANGUAGE LambdaCase #-}
|
||||
module Hasklet2 where
|
||||
import Control.Applicative (liftA2)
|
||||
import qualified Control.Applicative as CA
|
||||
import Data.Bifunctor
|
||||
|
||||
--
|
||||
-- * Parser type
|
||||
--
|
||||
|
||||
-- | Given a string, a parser either fails or returns a parsed value and
|
||||
-- the rest of the string to be parsed.
|
||||
newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }
|
||||
|
||||
instance Functor Parser where
|
||||
fmap f (Parser nf) = Parser $ (first f<$>) <$> nf
|
||||
|
||||
instance Applicative Parser where
|
||||
pure v = Parser $ Just . (,) v
|
||||
pf <*> pa = Parser $ \s -> do
|
||||
(f, s') <- runParser pf s
|
||||
(v, s'') <- runParser pa s'
|
||||
return (f v, s'')
|
||||
|
||||
--
|
||||
-- * Single character parsers
|
||||
--
|
||||
|
||||
-- | Match the end of the input string.
|
||||
end :: Parser ()
|
||||
end = Parser $ \case
|
||||
"" -> Just ((), "")
|
||||
_ -> Nothing
|
||||
|
||||
-- | Return the next character if it satisfies the given predicate.
|
||||
nextIf :: (Char -> Bool) -> Parser Char
|
||||
nextIf f = Parser $ \case
|
||||
(c:s') | f c -> Just (c,s')
|
||||
_ -> Nothing
|
||||
|
||||
-- | Parse the given character.
|
||||
char :: Char -> Parser Char
|
||||
char c = nextIf (c ==)
|
||||
|
||||
-- | Parse one of the given characters.
|
||||
oneOf :: [Char] -> Parser Char
|
||||
oneOf cs = nextIf (`elem` cs)
|
||||
|
||||
-- | Parse a particular class of character.
|
||||
lower, upper, digit, space :: Parser Char
|
||||
lower = oneOf ['a'..'z']
|
||||
upper = oneOf ['A'..'Z']
|
||||
digit = oneOf ['0'..'9']
|
||||
space = oneOf " \t\n\r"
|
||||
|
||||
-- | Parse a digit as an integer.
|
||||
digitInt :: Parser Int
|
||||
digitInt = flip (-) (fromEnum '0') . fromEnum <$> digit
|
||||
|
||||
--
|
||||
-- * Alternative and repeating parsers
|
||||
--
|
||||
|
||||
-- | Run the first parser. If it succeeds, return the result. Otherwise run
|
||||
-- the second parser.
|
||||
--
|
||||
-- >>> runParser (upper <|> digit) "Hi"
|
||||
-- Just ('H',"i")
|
||||
--
|
||||
-- >>> runParser (upper <|> digit) "42"
|
||||
-- Just ('4',"2")
|
||||
--
|
||||
-- >>> runParser (upper <|> digit) "w00t"
|
||||
-- Nothing
|
||||
--
|
||||
(<|>) :: Parser a -> Parser a -> Parser a
|
||||
p1 <|> p2 = Parser $ \s -> runParser p1 s CA.<|> runParser p2 s
|
||||
|
||||
|
||||
-- | Parse a sequence of one or more items, returning the results as a list.
|
||||
-- Parses the longest possible sequence (i.e. until the given parser fails).
|
||||
--
|
||||
-- >>> runParser (many1 lower) "abcDEF123"
|
||||
-- Just ("abc","DEF123")
|
||||
--
|
||||
-- >>> runParser (many1 lower) "ABCdef123"
|
||||
-- Nothing
|
||||
--
|
||||
-- >>> runParser (many1 (lower <|> upper)) "ABCdef123"
|
||||
-- Just ("ABCdef","123")
|
||||
--
|
||||
-- >>> runParser (many1 digitInt) "123abc"
|
||||
-- Just ([1,2,3],"abc")
|
||||
--
|
||||
many1 :: Parser a -> Parser [a]
|
||||
many1 p = liftA2 (:) p (many p)
|
||||
|
||||
|
||||
-- | Parse a sequence of zero or more items, returning the results as a list.
|
||||
--
|
||||
-- >>> runParser (many lower) "abcDEF123"
|
||||
-- Just ("abc","DEF123")
|
||||
--
|
||||
-- >>> runParser (many lower) "ABCdef123"
|
||||
-- Just ("","ABCdef123")
|
||||
--
|
||||
-- >>> runParser (many (lower <|> upper)) "abcDEF123"
|
||||
-- Just ("abcDEF","123")
|
||||
--
|
||||
-- >>> runParser (many digitInt) "123abc"
|
||||
-- Just ([1,2,3],"abc")
|
||||
--
|
||||
-- >>> runParser (many digitInt) "abc123"
|
||||
-- Just ([],"abc123")
|
||||
--
|
||||
many :: Parser a -> Parser [a]
|
||||
many p = liftA2 (:) p (many p) <|> pure []
|
||||
|
||||
|
||||
-- | Parse a natural number into a Haskell integer.
|
||||
--
|
||||
-- >>> runParser nat "123abc"
|
||||
-- Just (123,"abc")
|
||||
--
|
||||
-- >>> runParser nat "abc"
|
||||
-- Nothing
|
||||
--
|
||||
nat :: Parser Int
|
||||
nat = foldl ((+).(*10)) 0 <$> many1 digitInt
|
||||
|
||||
parenth :: Parser a -> Parser b -> Parser (a, b)
|
||||
parenth p1 p2 = liftA2 (,) (char '(' *> p1 <* char ',') (p2 <* char ')')
|
||||
|
||||
--
|
||||
-- * Parsing structured data
|
||||
--
|
||||
|
||||
-- | Parse a pair of natural numbers into a Haskell pair of integers. You can
|
||||
-- assume there are no spaces within the substring encoding the pair,
|
||||
-- although you're welcome to try to generalize it to handle whitespace too,
|
||||
-- e.g. before/after parentheses and the comma.
|
||||
--
|
||||
-- This may get a little bit hairy, but the ugliness here will motivate some
|
||||
-- key abstractions later. :-)
|
||||
--
|
||||
-- >>> runParser natPair "(123,45) 678"
|
||||
-- Just ((123,45)," 678")
|
||||
--
|
||||
-- >>> runParser natPair "(123,45"
|
||||
-- Nothing
|
||||
--
|
||||
-- >>> runParser natPair "(123,x) 678"
|
||||
-- Nothing
|
||||
--
|
||||
natPair = parenth nat nat
|
||||
|
||||
|
||||
-- | A simple tree data structure, isomorphic to arbitrarily nested pairs with
|
||||
-- integers at the leaves.
|
||||
data Tree
|
||||
= Leaf Int
|
||||
| Node Tree Tree
|
||||
deriving (Eq,Show)
|
||||
|
||||
|
||||
-- | Parse a tree encoded as arbitrarily nested pairs. This is basically just
|
||||
-- the 'natPair' parser, now with recursion.
|
||||
--
|
||||
-- >>> runParser natTree "((1,2),3) abc"
|
||||
-- Just (Node (Node (Leaf 1) (Leaf 2)) (Leaf 3)," abc")
|
||||
--
|
||||
-- >>> runParser natTree "(1,((100,101),10))"
|
||||
-- Just (Node (Leaf 1) (Node (Node (Leaf 100) (Leaf 101)) (Leaf 10)),"")
|
||||
--
|
||||
natTree :: Parser Tree
|
||||
natTree = (uncurry Node <$> parenth natTree natTree) <|> (Leaf <$> nat)
|
||||
|
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 :-)
|
Loading…
Reference in New Issue
Block a user