Add the first two homework assignments.
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user