8.3 KiB
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 afoldr. 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 thefoldrapplication (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-stylex ? 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
cxforcontains 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 thecxabstraction 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):data Tree a = Leaf a | Node (Tree a) (Tree a)You could then define a
containsTreefunction like this:containsTree :: Eq a => a -> Tree a -> Bool containsTree a = ct where ct (Leaf x) = a == x ct (Node l r) = ct l || ct rNote that here we no longer need to pass around the
ain recursive calls. This would become especially good ifTreehad more cases (which would all have recursive calls). We used this inXtrato implement the evaluation function for expressions - instead of passing around the environmentenv, we captured it like we capturedain the above example. -
You defined your
foldTreedifferently 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 dofoldTree (flip (:)) []. This makes it easy to express sequential computations, like for instance those forsumandcontains. In fact, you can even re-use list-based functions like so:toList = foldTree (flip (:)) [] sumTree = sum . toList containsTree a = contains a . toListIn short, your approach makes it really easy to express some computations. However, unlike
foldfor lists, you cannot usefoldTreeto define any function on trees. Consider the simple example ofdepth, and two trees:Node 1 (Node 2 (Node 3 Leaf Leaf) Leaf) LeafNode 1 (Node 2 Leaf Leaf) (Node 3 Leaf Leaf)
If you run them through
toList, you'll notice that they produce the same result. Yourb -> a -> bfunction 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
foldfor lists, but it makes it possible to define any function for lists. For example, here's that depth function I mentioned earlier:depth = foldTree (\_ l r -> 1 + max l r) 1And, of course, my
levelsfunction is also implemented usingfoldTree, 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 mya + b + cfunction 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 depthn, doesn't your approach performntraversals of the tree, once for each depth? This would mean you check1 + (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 writeif (n == m) then True else .... As I mentioned to Ben, this is very similar to writingn == m ? true : falsein C/C++: I'd say it's a bit of an antipattern. Specifically, the short-circuiting||operator will do exactly that; you can writen == n || ..., instead. -
There's a slight issue with your
foldTreefunction, which is what caused to have trouble withcontainsFold. Take a look at your signature:foldTree :: (a -> a -> a) -> a -> Tree a -> aNote the very last part:
Tree a -> a. This means that you can only use yourtreeFoldfunction to produce the same type of values that are in the tree! This works forsumTreeFold, because numbers are closed under addition; it doesn't, however, work forcontainsTreeFold, 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 variablebalongsidea. This is strictly more general than using onlyaeverywhere:bcan be equal toa(much likexandycan be equal in an equation), but it can also be different. Thus, yoursumTreeFoldwould still work, but you'd be able to writecontainsTreeFoldas 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 getlowerLevels? Suppose thatlthas the levels[1,2], [3,4,5,6]andrthas 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 functionzipWithfrom the standard library in Haskell; However, the problem is thatzipWithstops 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:
Our final function implementation is then: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.
I implemented mine using my customlevels Leaf = [] levels (Node m lt rt) = [m] : lowerLevels where lowerLevels = myZipWith lt rtfold, but in essense it works the same way. MymyZipWithis calledpadded, but the implementation is identical to what I showed here.
- For a leaf, there are no levels, so your solution would just be