Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solutions for Lecture 2 exercises #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 108 additions & 13 deletions src/Lecture2.hs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ module Lecture2
, constantFolding
) where

import Data.Char ( isSpace )
import Data.List ( sort )

{- | Implement a function that finds a product of all the numbers in
the list. But implement a lazier version of this function: if you see
zero, you can stop calculating product and return 0 immediately.
Expand All @@ -48,7 +51,13 @@ zero, you can stop calculating product and return 0 immediately.
84
-}
lazyProduct :: [Int] -> Int
lazyProduct = error "TODO"
lazyProduct [] = 1
lazyProduct list = go 1 list
Comment on lines +54 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me that the first case is redundant because your helper function go will return 1 immediately if the given list is empty.

After you remove this case, you can also eta-reduce the top-level function ✂️

Suggested change
lazyProduct [] = 1
lazyProduct list = go 1 list
lazyProduct = go 1

where
go :: Int -> [Int] -> Int
go 0 _ = 0
go result [] = result
go result (x : xs) = go (result * x) xs
Comment on lines +58 to +60

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very clever solution! 🧠
I'm not sure if you know about BangPatterns or strict evaluations in Haskell (I'm going to cover them tomorrow in Lecture 3) but your solution also has the optimal performance behaviour 😅

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks :-) In the past, I have mostly been reading about Haskell, haven't written any code. I have come across BangPatterns and strict evaluation in my readings, but never applied them as I've not actually written any Haskell code.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by the way @chshersh curious, how do you get these cool emojis in your comments?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kpadmasola You can start typing : and then the name of an emoji. GitHub has some autocompletion for emoji names 🙂 Like :smile or :+1.


{- | Implement a function that duplicates every element in the list.

Expand All @@ -58,7 +67,8 @@ lazyProduct = error "TODO"
"ccaabb"
-}
duplicate :: [a] -> [a]
duplicate = error "TODO"
duplicate [] = []
duplicate (x : xs) = x : x : duplicate xs

{- | Implement function that takes index and a list and removes the
element at the given position. Additionally, this function should also
Expand All @@ -70,7 +80,16 @@ return the removed element.
>>> removeAt 10 [1 .. 5]
(Nothing,[1,2,3,4,5])
-}
removeAt = error "TODO"
removeAt :: Int -> [a] -> (Maybe a, [a])
removeAt _ [] = (Nothing, [])
removeAt index list
| index < 0 = (Nothing, list)
| otherwise = (element, headPart ++ tailPart)
where
headPart = take index list
(element, tailPart) = case drop index list of
Comment on lines +89 to +90

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a correct implementation 👍🏻
Alternatively, you can use a standard function splitAt instead of using take and drop to avoid traversing the list prefix twice 🔃

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point.

[] -> (Nothing, [])
(x: xs) -> (Just x, xs)

{- | Write a function that takes a list of lists and returns only
lists of even lengths.
Expand All @@ -81,7 +100,8 @@ lists of even lengths.
♫ NOTE: Use eta-reduction and function composition (the dot (.) operator)
in this function.
-}
evenLists = error "TODO"
evenLists :: [[a]] -> [[a]]
evenLists = filter (even . length)

{- | The @dropSpaces@ function takes a string containing a single word
or number surrounded by spaces and removes all leading and trailing
Expand All @@ -97,7 +117,8 @@ spaces.

🕯 HINT: look into Data.Char and Prelude modules for functions you may use.
-}
dropSpaces = error "TODO"
dropSpaces :: String -> String
dropSpaces = takeWhile (not . isSpace) . dropWhile isSpace

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect 🏆

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks !

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chshersh One doubt I have is, how does this efficiently handle infinite trailing spaces? I noticed that there is a testcase for that. Without looking at all the infinite trailing spaces, how can this work? Is it some kind of fusion or something like that?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no special magic 🙂
takeWhile will just stop on the first space character and won't traverse the list any further. It works due to laziness.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks!


{- |

Expand Down Expand Up @@ -159,8 +180,43 @@ data Knight = Knight
, knightAttack :: Int
, knightEndurance :: Int
}

dragonFight = error "TODO"
data Color
= Red
| Green
| Black


data Treasure
= Gold
| GoldPlusTreasure

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I clarified the task a bit to cover cases so dragons would determine the treasure itself, not just "yes" or "no". It will make the implementation a bit more challenging 😅

But, besides that, the current implementation covers the requirements nicely 👍🏻


data Dragon = Dragon
{ dragonColor :: Color
, dragonExperiencePoints :: Int
, dragonTreasure :: Treasure
, dragonFirePower :: Int
, dragonHealth :: Int
}
Comment on lines +193 to +199

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great idea! And almost the perfect solution 🔝

There're two problems with this particular data type definition:

  1. It's possible to have a treasure inside green dragon:
myDragon = Dragon
    { dragonColor = Green
    , dragonExperiencePoints = 50
    , dragonTreasure = GoldPlusTreasure
    , dragonFirePower = 70
    , dragonHealth = 150
    }
  1. It's possible to not have a treasure inside non-greed dragons:
myDragon = Dragon
    { dragonColor = Red
    , dragonExperiencePoints = 50
    , dragonTreasure = Gold
    , dragonFirePower = 70
    , dragonHealth = 150
    }

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chshersh You are right, I know I cut the corners a bit on this one :-) I enjoyed this particular exercise a lot as it was like a fantasy story with dragons, knights and treasures !! :-) I got the point about making invalid states unrepresentable, but could not make much progress towards it. I started watching the video you suggested, but wasn't able to watch it completely yesterday. I'm planning to go over it again.

data FightOutcome
= DragonDies
| KnightDies
| KnightRunsAway

dragonFight :: Knight -> Dragon -> FightOutcome
dragonFight = go 0
where
go :: Int -> Knight -> Dragon -> FightOutcome
go n k d
| knightHealth k <= 0 = KnightDies
| dragonHealth d <= 0 = DragonDies
| knightEndurance k == 0 = KnightRunsAway
| otherwise = go n' k' d'
where
n' = n + 1
k' = k { knightEndurance = knightEndurance k - 1
, knightHealth = knightHealth k - if n' `mod` 10 == 0 then dragonFirePower d else 0
}
d' = d { dragonHealth = dragonHealth d - knightAttack k }

----------------------------------------------------------------------------
-- Extra Challenges
Expand All @@ -181,7 +237,11 @@ False
True
-}
isIncreasing :: [Int] -> Bool
isIncreasing = error "TODO"
isIncreasing [] = True
isIncreasing [_] = True
isIncreasing (x : y : rest)
| x > y = False
| otherwise = isIncreasing (y : rest)
Comment on lines +242 to +244

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can the implementation slightly more elegant by relying on laziness in Haskell 🦥

Suggested change
isIncreasing (x : y : rest)
| x > y = False
| otherwise = isIncreasing (y : rest)
isIncreasing (x : y : ys) = x < y && isIncreasing (y : ys)


{- | Implement a function that takes two lists, sorted in the
increasing order, and merges them into new list, also sorted in the
Expand All @@ -194,7 +254,11 @@ verify that.
[1,2,3,4,7]
-}
merge :: [Int] -> [Int] -> [Int]
merge = error "TODO"
merge xs [] = xs
merge [] ys = ys
merge (x : xs) (y : ys)
| x < y = x : merge xs (y : ys)
| otherwise = y : merge (x : xs) ys
Comment on lines +259 to +261

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely correct solution! 💎
You can make it more performant by checking if both heads of the given lists are equal. In that case, you're able to reduce the size of each list by 1 on each recursive call instead of reducing the size of only a single list ✂️

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks!


{- | Implement the "Merge Sort" algorithm in Haskell. The @mergeSort@
function takes a list of numbers and returns a new list containing the
Expand All @@ -211,8 +275,11 @@ The algorithm of merge sort is the following:
[1,2,3]
-}
mergeSort :: [Int] -> [Int]
mergeSort = error "TODO"

mergeSort [] = []
mergeSort [x] = [x]
mergeSort xs = merge (sort ps) (sort qs)
where
(ps, qs) = splitAt (length xs `div` 2) xs

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use splitAt instead of take + drop to avoid traversing the first half twice.

Additionally, you can avoid using length if you notice that you don't necessarily need to split in the middle. You only need to split it into two lists of approximately the same size.

In that case, you can implement a solution that traverses a list only once and splits its elements into two lists of elements on only even positions and only odd positions.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need to spend some more thought on this...


{- | Haskell is famous for being a superb language for implementing
compilers and interpeters to other programming languages. In the next
Expand Down Expand Up @@ -264,7 +331,16 @@ data EvalError
It returns either a successful evaluation result or an error.
-}
eval :: Variables -> Expr -> Either EvalError Int
eval = error "TODO"
eval _ (Lit i) = Right i
eval symbolTable (Var symbol) = case lookup symbol symbolTable of
Nothing -> Left (VariableNotFound symbol)
Just value -> Right value
eval symbolTable (Add e1 e2) = addExpr (eval symbolTable e1) (eval symbolTable e2)
where
addExpr :: Either EvalError Int -> Either EvalError Int -> Either EvalError Int
addExpr (Left x) _ = Left x
addExpr _ (Left x) = Left x
addExpr (Right x) (Right y) = Right (x + y)
Comment on lines +340 to +343

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great helper function! 👏🏻
Once you learn about Monads, you'll be able to reduce this function to a single line ✂️

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!


{- | Compilers also perform optimizations! One of the most common
optimizations is "Constant Folding". It performs arithmetic operations
Expand All @@ -288,4 +364,23 @@ Write a function that takes and expression and performs "Constant
Folding" optimization on the given expression.
-}
constantFolding :: Expr -> Expr
constantFolding = error "TODO"
constantFolding = addExpr . extractConstant
where
extractConstant :: Expr -> ([Int], [Expr])
extractConstant (Lit i) = ([i], [])
extractConstant (Var x) = ([], [Var x])
extractConstant (Add x y) = (x1 ++ y1, x2 ++ y2)
where
(x1, x2) = extractConstant x
(y1, y2) = extractConstant y
addExpr :: ([Int], [Expr]) -> Expr
addExpr (c, e) = case (c, e) of
([], []) -> error "invalid"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! You can see that it's actually impossible to have both lists empty at the same time. So here you're handling and impossible situation 🙂

Can you maybe change types a bit to avoid having this error? 😉

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chshersh :-) I know you mean "making invalid states unrepresentable", need to think about this some more ...

(xs, []) -> Lit (sum xs)
([], ys) -> addVars ys
(xs, ys) -> if sum xs == 0 then addVars ys else Add (Lit (sum xs)) (addVars ys)
addVars :: [Expr] -> Expr
addVars [] = error "Invalid"
addVars [x] = x
addVars [x, y] = Add x y
addVars (x : y : rest) = Add (Add x y) (addVars rest)
Comment on lines +380 to +386

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get rid of handling this impossible case too. In the pair you already pattern matched on the list, so you know that it's not empty. So yo can pass it's first element and the rest to the function and continue doing this recursively to avoid throwing error

Suggested change
([], ys) -> addVars ys
(xs, ys) -> if sum xs == 0 then addVars ys else Add (Lit (sum xs)) (addVars ys)
addVars :: [Expr] -> Expr
addVars [] = error "Invalid"
addVars [x] = x
addVars [x, y] = Add x y
addVars (x : y : rest) = Add (Add x y) (addVars rest)
([], y : ys) -> addVars y ys
(xs, ys) -> if sum xs == 0 then addVars ys else Add (Lit (sum xs)) (addVars ys)
addVars :: Expr -> [Expr] -> Expr
addVars e [] = e
addVars e (x : xs) = ...

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @chshersh , will need to think about this some more.