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 Cstyle `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 shortcircuiting


`()` 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 reuse listbased 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 pointfree 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 casebycase 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 shortcircuiting `` 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 rederive 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


eyeopening to contribute, but here goes:




* At first glance, it seemed like it should be _easy_ to simplify all those 4deep 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` statepassing 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 4deep 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