Assignment 2 solutions have now been released, which you can find here. As usual, you are encouraged to look at the solutions to see how they work.
We have also provided the marking scripts for you to see where your marks have come from and to get feedback on each function. To run the marking, you should do the following:
- Do a
git pullfrom the terminal in thefp-learning-2025folder on vLab - this will update your repo on vLab to have the updated files. - Navigate to the
Assignment 2folder - this is where the marking script should be run. - Here, you should have your assignment file; ensure this file is called
Assignment2.hs. - In the same folder, in the terminal (not in ghci), run the following command:
echo 'main' | stack repl; this will download the dependencies and run the marking automatically for you. - The marking scripts print out the tests individually, accompanied with whether your attempt passes/failed the test, and if you pass, how many marks you get. After all the tests have been run, it then tells you your overall score for this assignment.
To solve the assignment, please copy the file Assignment2-Template.hs to a new file called Assignment2.hs and write your solutions in Assignment2.hs.
Don't change the header of this file, including the module declaration, and, moreover, don't change the type signature of any of the given functions for you to complete.
Run the pre-submit script to check for any (compilation) errors before submitting by running in the vLab terminal:
$ ./presubmit.sh Assignment2If this fails, you are not ready to submit, and any submission that doesn't pass the presubmit test on vLab is not eligible for marking.
Submit your file Assignment2.hs via Canvas.
International Morse code is composed of five elements:
- short mark, dot or "dit" (·) -- one unit long;
- longer mark, dash or "dah" (-) -- three units long;
- inter-element gap between the dots and dashes within a character -- one unit long;
- short gap (between letters) -- three units long;
- medium gap (between words) -- seven units long.
We represent a Morse code unit, called "atom" from now on, as either a beep or silence:
data Atom = Beep | Silence deriving (Eq, Show)Then Morse code can be represented by a list of these atoms:
type Code = [Atom]Now we can write constants for a short mark "·" (dit), a long mark "-" (dah), a gap between letters (shortGap) and a gap between words (mediumGap):
dit, dah, shortGap, mediumGap :: Code
dit = [Beep, Silence]
dah = [Beep, Beep, Beep, Silence]
shortGap = replicate (3-1) Silence
mediumGap = replicate (7-1) SilenceNote that the length of shortGap and mediumGap are made so that a
shortGap has the correct length 3 if following a dit or dah and
a mediumGap has length 7 if following a dit or dah.
This is because dit and dah each end in a Silence by definition.
We can code symbols as follows
morseCode :: Char -> Code
morseCode 'A' = dit ++ dah
morseCode 'B' = dah ++ dit ++ dit ++ dit
morseCode 'C' = dah ++ dit ++ dah ++ dit
morseCode 'D' = dah ++ dit ++ dit
morseCode 'E' = dit
morseCode 'F' = dit ++ dit ++ dah ++ dit
morseCode 'G' = dah ++ dah ++ dit
morseCode 'H' = dit ++ dit ++ dit ++ dit
morseCode 'I' = dit ++ dit
morseCode 'J' = dit ++ dah ++ dah ++ dah
morseCode 'K' = dah ++ dit ++ dah
morseCode 'L' = dit ++ dah ++ dit ++ dit
morseCode 'M' = dah ++ dah
morseCode 'N' = dah ++ dit
morseCode 'O' = dah ++ dah ++ dah
morseCode 'P' = dit ++ dah ++ dah ++ dit
morseCode 'Q' = dah ++ dah ++ dit ++ dah
morseCode 'R' = dit ++ dah ++ dit
morseCode 'S' = dit ++ dit ++ dit
morseCode 'T' = dah
morseCode 'U' = dit ++ dit ++ dah
morseCode 'V' = dit ++ dit ++ dit ++ dah
morseCode 'W' = dit ++ dah ++ dah
morseCode 'X' = dah ++ dit ++ dit ++ dah
morseCode 'Y' = dah ++ dit ++ dah ++ dah
morseCode 'Z' = dah ++ dah ++ dit ++ dit
morseCode '1' = dit ++ dah ++ dah ++ dah ++ dah
morseCode '2' = dit ++ dit ++ dah ++ dah ++ dah
morseCode '3' = dit ++ dit ++ dit ++ dah ++ dah
morseCode '4' = dit ++ dit ++ dit ++ dit ++ dah
morseCode '5' = dit ++ dit ++ dit ++ dit ++ dit
morseCode '6' = dah ++ dit ++ dit ++ dit ++ dit
morseCode '7' = dah ++ dah ++ dit ++ dit ++ dit
morseCode '8' = dah ++ dah ++ dah ++ dit ++ dit
morseCode '9' = dah ++ dah ++ dah ++ dah ++ dit
morseCode '0' = dah ++ dah ++ dah ++ dah ++ dah
morseCode _ = undefined -- Avoid warningsWe can represent the function morseTable by the following lookup table:
type Table = [(Char, Code)]
morseTable :: Table
morseTable = [ (c , morseCode c) | c <- ['A'..'Z']++['0'..'9'] ]Remark: We can get the function morseCode from the table using
lookup :: Eq a => a -> [(a, b)] -> Maybe bfrom the prelude.
Remark: The above datatypes, constants, morseCode and morseTable are all
available in Types.hs. Do not change this file.
You may wish to use your own helper functions.
In the following parts of this exercise, the functions should work with any
table, including morseTable but not only morseTable. You can use morseTable
for testing your solution for correctness.
-
Write a function
encodeWord :: Table -> String -> Code
that, given any table and a string with a single word in it, produces the code for that word. The resulting code should have a
shortGapafter every character that is not the last.Note that
encodeWordshould work for any table, not justmorseTable.Examples:
encodeWord morseTable "" = [] encodeWord morseTable "A" = [Beep,Silence,Beep,Beep,Beep,Silence] encodeWord morseTable "0" = [Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence] encodeWord morseTable "HELLO" = [Beep,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence] encodeWord morseTable "WORLD" = [Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Beep,Silence]
-
Write a function
encodeWords :: Table -> [String] -> Code
that, given any table and a list of strings, encodes each string of the list, puts a
mediumGapafter every word that is not the last, and concatenates the results.As above, this function should work for any table, not just
morseTable.Examples:
encodeWords morseTable [] = [] encodeWords morseTable ["007"] = [Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Beep,Silence,Beep,Silence] encodeWords morseTable ["HI","THERE"] = [Beep,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Silence,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence]
-
Write a function
encodeText :: Table -> String -> Code
that, given any table and some string of words separated by spaces, encodes the text. Spaces should become
mediumGaps.You may assume that the text consists only of single spaces and
['A'..'Z']++['0'..'9']otherwise, and that the spaces do not occur at the end.Examples:
encodeText morseTable "WORD" = [Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Beep,Silence] encodeText morseTable "HI THERE" = [Beep,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Silence,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence] encodeText morseTable "THIS IS A TEST" = [Beep,Beep,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Silence,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Silence,Silence,Silence,Silence,Beep,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Silence,Silence,Silence,Silence,Beep,Beep,Beep,Silence,Silence,Silence,Beep,Silence,Silence,Silence,Beep,Silence,Beep,Silence,Beep,Silence,Silence,Silence,Beep,Beep,Beep,Silence]
HINT: You may find it useful to define a general function
split :: Eq a => [a] -> [a] -> [[a]]
satisfying the following specification:
split " " "ABC DEF" = ["ABC","DEF"] split shortGap (dit ++ shortGap ++ dah) = [dit,dah]
where the first argument of
splitindicates a word separator, which is " " in this example.
Write a function
decodeText :: Table -> Code -> Stringsuch that
decodeText t (encodeText t s) = sfor every s :: String and t :: Table. We will only test strings with capital letters, digits and spaces.
For instance, we should have:
decodeText morseTable (encodeText morseTable "THIS IS A TEST") = "THIS IS A TEST"HINT: You may find it helpful to use your general function split discussed
above.
We can also store tables as trees where a left branch is a dit and a right
branch is a dah. For Morse code, we would get the following tree.
Here is the type of such trees.
data Tree = Empty
| Branch (Maybe Char) Tree Tree
deriving (Show , Eq)Write a function
decodeTextWithTree :: Tree -> Code -> Stringthat decodes using a tree such as the above.
This function should work with any such tree, not just the tree pictured above.
For testing purposes, here is the tree above in Haskell:
let morseTree = Branch Nothing (Branch (Just 'E') (Branch (Just 'I') (Branch (Just 'S') (Branch (Just 'H') (Branch (Just '5') Empty Empty) (Branch (Just '4') Empty Empty)) (Branch (Just 'V') Empty (Branch (Just '3') Empty Empty))) (Branch (Just 'U') (Branch (Just 'F') Empty Empty) (Branch Nothing Empty (Branch (Just '2') Empty Empty)))) (Branch (Just 'A') (Branch (Just 'R') (Branch (Just 'L') Empty Empty) Empty) (Branch (Just 'W') (Branch (Just 'P') Empty Empty) (Branch (Just 'J') Empty (Branch (Just '1') Empty Empty))))) (Branch (Just 'T') (Branch (Just 'N') (Branch (Just 'D') (Branch (Just 'B') (Branch (Just '6') Empty Empty) Empty) (Branch (Just 'X') Empty Empty)) (Branch (Just 'K') (Branch (Just 'C') Empty Empty) (Branch (Just 'Y') Empty Empty))) (Branch (Just 'M') (Branch (Just 'G') (Branch (Just 'Z') (Branch (Just '7') Empty Empty) Empty) (Branch (Just 'Q') Empty Empty)) (Branch (Just 'O') (Branch Nothing (Branch (Just '8') Empty Empty) Empty) (Branch Nothing (Branch (Just '9') Empty Empty) (Branch (Just '0') Empty Empty)))))Your function decodeTextWithTree should satisfy for instance:
decodeTextWithTree morseTree (encodeText morseTable "THIS IS ANOTHER TEST") = "THIS IS ANOTHER TEST"Write a function
ramify :: Table -> Treethat translates a given Table into a Tree. For example ramify morseTable should give the tree pictured above, i.e.
ramify morseTable = Branch Nothing (Branch (Just 'E') (Branch (Just 'I') (Branch (Just 'S') (Branch (Just 'H') (Branch (Just '5') Empty Empty) (Branch (Just '4') Empty Empty)) (Branch (Just 'V') Empty (Branch (Just '3') Empty Empty))) (Branch (Just 'U') (Branch (Just 'F') Empty Empty) (Branch Nothing Empty (Branch (Just '2') Empty Empty)))) (Branch (Just 'A') (Branch (Just 'R') (Branch (Just 'L') Empty Empty) Empty) (Branch (Just 'W') (Branch (Just 'P') Empty Empty) (Branch (Just 'J') Empty (Branch (Just '1') Empty Empty))))) (Branch (Just 'T') (Branch (Just 'N') (Branch (Just 'D') (Branch (Just 'B') (Branch (Just '6') Empty Empty) Empty) (Branch (Just 'X') Empty Empty)) (Branch (Just 'K') (Branch (Just 'C') Empty Empty) (Branch (Just 'Y') Empty Empty))) (Branch (Just 'M') (Branch (Just 'G') (Branch (Just 'Z') (Branch (Just '7') Empty Empty) Empty) (Branch (Just 'Q') Empty Empty)) (Branch (Just 'O') (Branch Nothing (Branch (Just '8') Empty Empty) Empty) (Branch Nothing (Branch (Just '9') Empty Empty) (Branch (Just '0') Empty Empty)))))Write a function
tabulate :: Tree -> Tablethat does the opposite. That is, it should satisfy:
ramify (tabulate t) = tfor every t :: Tree.
(Note that this establishes Tree as a
retract
of Table. The types are not isomorphic, because, in general, tabulate (ramify t) may be only a permutation of the table t.)
Consider the following type of "bracket trees" with constructors Round :: [Bracket] -> Bracket and Curly :: [Bracket] -> Bracket:
data Bracket = Round [Bracket] | Curly [Bracket] deriving (Show,Eq)The simplest trees we can construct are Round [] and Curly []. Other examples of such trees are
Round [Curly [] , Round []] and Round [Curly [] , Round [] , Curly [ Round [] ]].
We can convert bracket trees to strings:
brackets :: Bracket -> String
brackets (Round ts) = "(" ++ concat [brackets t | t <- ts] ++ ")"
brackets (Curly ts) = "{" ++ concat [brackets t | t <- ts] ++ "}"Write a partial inverse
tree :: String -> Maybe Bracketof the function brackets satisfying the following specification.
For any bracket tree t, we should have that
- the equation
tree (brackets t) = Just tholds,
and we should have that for any string xs,
- the equation
tree xs = Nothingholds precisely whenxsis not well-bracketed.
Well-bracketed strings are defined by the following two generating rules:
-
Any string starting with '(' followed by zero or more well-bracketed strings and ending with ')' is well bracketed.
-
Any string starting with '{' followed by zero or more well-bracketed strings and ending with '}' is well bracketed.
It is a fact that bracket trees generate only well bracketed strings, using the function brackets, and, moreover, every well-bracketed string arises from some bracket tree in this way.
Examples:
- "()" is well bracketed
- "{}" is well bracketed
- "({})" is well bracketed
- "{()()}" is well bracketed
- "(()(){()()})" is well bracketed
- "(((){}){}(()()))" is well bracketed
- "(()" is not
- "())" is not
- "(()(()()" is not
- "(}" is not
- "({)}" is not
- "(cat)" is not
- "" is not
Then we can use the function tree to check whether an expression is well bracketed:
isWellBracketed :: String -> Bool
isWellBracketed xs = case tree xs of
Nothing -> False
Just _ -> TrueYou can use this function for testing. You can also test by creating well-bracketed strings xsand checking whether
brackets (fromJust (tree xs)) = xswhere fromJust is in the import Data.Maybe.
