In this homework we will gradually develop a small programming language called Hi.
Create a .cabal file with both a library and an executable:
library
  exposed-modules:    ...
  build-depends:      ...
  ...
executable hi
  main-is:            Main.hs
  hs-source-dirs:     ...
  build-depends:      ...
  ...In the library component, create the following modules:
Hi.Base
Hi.Parser
Hi.Pretty
Hi.Evaluator
You are allowed to add more modules to the project, but those are the required ones.
- 
In Hi.Base, define the following data types:data HiFun -- function names (e.g. div, sort, length, ...) data HiValue -- values (numbers, booleans, strings, ...) data HiExpr -- expressions (literals, function calls, ...) data HiError -- evaluation errors (invalid arguments, ...) In each task, we will add constructors to these data types that are needed to implement new language features. 
- 
In Hi.Parser, define the following function:parse :: String -> Either (ParseErrorBundle String Void) HiExpr The ParseErrorBundle type comes from the megaparsec package which we will use to implement our parser. 
- 
In Hi.Pretty, define the following function:prettyValue :: HiValue -> Doc AnsiStyle The DocandAnsiStyletypes come from theprettyprinterandprettyprinter-ansi-terminalpackages respectively. This function renders a value to a document, which in turn can be either printed to the terminal (with color highlighting) or converted to a string.
- 
In Hi.Evaluator, define the following function:eval :: Monad m => HiExpr -> m (Either HiError HiValue) One might wonder why we need the Monad m part. Indeed, for arithmetic operations, a simpler type would be sufficient: eval :: HiExpr -> Either HiError HiValue However, the monadic context will come into play later, when we start implementing IO actions (file system access, random number generation, and so on). 
The executable component consists just of a single file, Main.hs.
Using the haskeline package, implement a REPL in Main.hs that uses parse, eval, and prettyValue defined above. It’s going to be just 15–20 lines of code, but you will use it all the time to test your implementation.
Here’s an example session that will become possible as soon as we implement arithmetic operations:
hi> mul(2, 10)
20
hi> sub(1000, 7)
993
hi> div(3, 5)
0.6
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunDiv | HiFunMul | HiFunAdd | HiFunSub data HiValue = ... | HiValueNumber Rational | HiValueFunction HiFun data HiExpr = ... | HiExprValue HiValue | HiExprApply HiExpr [HiExpr] data HiError = ... | HiErrorInvalidArgument | HiErrorInvalidFunction | HiErrorArityMismatch | HiErrorDivideByZero Numbers are represented using the Rationaltype fromData.Ratio.
- 
In the parser, add support for the following constructs: - Built-in names div,mul,add, andsub, that are parsed into the correspondingHiFunconstructors (and then wrapped inHiValueFunction).
- Numeric literals, such as 2,3.14,-1.618, or1.2e5, that are parsed intoHiValueNumber(tip: useText.Megaparsec.Char.Lexer.scientific).
- Function application f(a, b, c, ...)that is parsed intoHiExprApply.
 For example, the expression div(add(10, 15.1), 3)is represented by the following syntax tree:HiExprApply (HiExprValue (HiValueFunction HiFunDiv)) [ HiExprApply (HiExprValue (HiValueFunction HiFunAdd)) [ HiExprValue (HiValueNumber (10 % 1)), HiExprValue (HiValueNumber (151 % 10)) ], HiExprValue (HiValueNumber (3 % 1)) ] 
- Built-in names 
- 
In the evaluator, implement the arithmetic operations: - add(500, 12)evaluates to- 512(addition)
- sub(10, 100)evaluates to- -90(subtraction)
- mul(23, 768)evaluates to- 17664(multiplication)
- div(57, 190)evaluates to- 0.3(division)
 Nested function applications are allowed: - div(add(mul(2, 5), 1), sub(11,6))evaluates to- 2.2
 The following errors must be returned as HiError:- HiErrorArityMismatch: functions called with an incorrect amount of arguments, e.g.- sub(1)or- sub(1, 2, 3).
- HiErrorDivideByZero: the- divfunction is called with- 0as its second argument, e.g.- div(1, 0)or- div(1, sub(5, 5)).
- HiErrorInvalidFunction: numbers are used in function positions, e.g.- 15(2).
- HiErrorInvalidArgument: functions are used in numeric positions, e.g.- sub(10, add).
 You are advised to use the ExceptTmonad transformer to propagateHiErrorthrough the evaluator.
- 
In the pretty-printer, define the following special cases for rendering numbers: - integers: 42,-8,15
- finite decimal fractions: 3.14,-8.15,77.01
- fractions: 1/3,-1/7,3/11
- mixed fractions: 5 + 1/3,-10 - 1/7,24 + 3/11
 You will find these functions useful: - quotRemfrom- Prelude
- fromRationalRepetendUnlimitedfrom the- scientificpackage
 
- integers: 
The following session in the REPL should be possible if you have implemented all of the above correctly:
hi> 100
100
hi> -15
-15
hi> add(100, -15)
85
hi> add(3, div(14, 100))
3.14
hi> div(10, 3)
3 + 1/3
hi> sub(mul(201, 11), 0.33)
2210.67
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunNot | HiFunAnd | HiFunOr | HiFunLessThan | HiFunGreaterThan | HiFunEquals | HiFunNotLessThan | HiFunNotGreaterThan | HiFunNotEquals | HiFunIf data HiValue = ... | HiValueBool Bool 
- 
In the parser, add support for the following constructs: - Built-in names not,and,or,less-than,greater-than,equals,not-less-than,not-greater-than,not-equals,if, that are parsed into the correspondingHiFunconstructors.
- Built-in names trueandfalsethat are parsed intoHiValueBool.
 
- Built-in names 
- 
In the evaluator, implement the new operations. Boolean algebra: - not(true)evaluates to- false(negation)
- and(true, false)evaluates to- false(conjunction)
- or(true, false)evaluates to- true(disjunction)
 Equality checking: - equals(10, 10)evaluates to- true
- equals(false, false)evaluates to- true
- equals(3, 10)evaluates to- false
- equals(1, true)evaluates to- false(no implicit cast)
 Comparisons: - less-than(3, 10)evaluates to- true
- less-than(false, true)evaluates to- true
- less-than(false, 0)evaluates to- true(- Boolis less than- Number)
 Complements: - for all AB,greater-than(A, B) ≡ less-than(B, A)holds
- for all AB,not-equals(A, B) ≡ not(equals(A, B))holds
- for all A,B,not-less-than(A, B) ≡ not(less-than(A, B))holds
- for all A,B,not-greater-than(A, B) ≡ not(greater-than(A, B))holds
 Branching: - for all AB,if(true, A, B) ≡ Aholds
- for all AB,if(false, A, B) ≡ Bholds
 
The following session in the REPL should be possible:
hi> false
false
hi> equals(add(2, 2), 4)
true
hi> less-than(mul(999, 99), 10000)
false
hi> if(greater-than(div(2, 5), div(3, 7)), 1, -1)
-1
hi> and(less-than(0, 1), less-than(1, 0))
false
Note also that functions are values:
hi> if(true, add, mul)
add
hi> if(true, add, mul)(10, 10)
20
hi> if(false, add, mul)(10, 10)
100
Functions can also be tested for equality:
hi> equals(add, add)
true
hi> equals(add, mul)
false
The check is trivial: a function is equal only to itself.
Ordering of function symbols is implementation-defined, that is, it’s up to you whether less-than(add, mul) or greater-than(add, mul).
In the parser, add support for infix operators. The precedence and associativity are the same as in Haskell.
For all A B:
- A / Bparses to- div(A, B)
- A * Bparses to- mul(A, B)
- A + Bparses to- add(A, B)
- A - Bparses to- sub(A, B)
- A < Bparses to- less-than(A, B)
- A > Bparses to- greater-than(A, B)
- A >= Bparses to- not-less-than(A, B)
- A <= Bparses to- not-greater-than(A, B)
- A == Bparses to- equals(A, B)
- A /= Bparses to- not-equals(A, B)
- A && Bparses to- and(A, B)
- A || Bparses to- or(A, B)
Tip: use makeExprParser from the parser-combinators package.
The following session in the REPL should be possible:
hi> 2 + 2
4
hi> 2 + 2 * 3
8
hi> (2 + 2) * 3
12
hi> 2 + 2 * 3 == (2 + 2) * 3
false
hi> 10 == 2*5 && 143 == 11*13
true
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunLength | HiFunToUpper | HiFunToLower | HiFunReverse | HiFunTrim data HiValue = ... | HiValueNull | HiValueString Text Strings are represented using the Texttype from thetextpackage.
- 
In the parser, add support for the following constructs: - Bulit-in names length,to-upper,to-lower,reverse,trim, that are parsed into the correspondingHiFunconstructors.
- Built-in name nullthat is parsed intoHiValueNull.
- String literals, such as "hello","42", or"header\nfooter", that are parsed intoHiValueString(tip: useText.Megaparsec.Char.Lexer.charLiteral).
 
- Bulit-in names 
- 
In the evaluator, implement the new operations: - length("Hello World")evaluates to- 11
- to-upper("Hello World")evaluates to- "HELLO WORLD"
- to-lower("Hello World")evaluates to- "hello world"
- reverse("stressed")evaluates to- "desserts"
- trim(" Hello World ")evaluates to- "Hello World"
 Then overload existing operations to work on strings: - "Hello" + "World"evaluates to- "HelloWorld"
- "Cat" * 5evaluates to- "CatCatCatCatCat"(tip: use- stimes)
- "/home/user" / "hi"evaluates to- "/home/user/hi"
 When a string is used as a function of one argument, perform a lookup: - "Hello World"(0)evaluates to- "H"
- "Hello World"(7)evaluates to- "o"
 Out-of-bounds indexing returns null:- "Hello World"(-1)evaluates to- null
- "Hello World"(99)evaluates to- null
 When a string is used as a function of two arguments, take a slice: - "Hello World"(0, 5)evaluates to- "Hello"
- "Hello World"(2, 4)evaluates to- "ll"
 
- 
(Advanced) When a slice index is negative, implement the Python semantics of indexing from the end of the string: - "Hello World"(0, -4)evaluates to- "Hello W"
- "Hello World"(-4, -1)evaluates to- "orl"
 When a slice index is null, treat it as the start/end of the string:- "Hello, World"(2, null)evaluates to- "llo, World"
- "Hello, World"(null, 5)evaluates to- "Hello"
 
The following session in the REPL should be possible:
hi> to-upper("what a nice language")(7, 11)
"NICE"
hi> "Hello" == "World"
false
hi> length("Hello" + "World")
10
hi> length("hehe" * 5) / 3
6 + 2/3
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunList | HiFunRange | HiFunFold data HiValue = ... | HiValueList (Seq HiValue) Lists are represented using the Seqtype from thecontainerspackage.
- 
In the parser, add support for the following constructs: - Built-in names list,range,fold, that are parsed into the correspondingHiFunconstructors.
- List literals, written as [A, B, C, ...], that are parsed into function applicationlist(A, B, C, ...).
 
- Built-in names 
- 
In the evaluator, implement the new operations: - list(1, 2, 3)constructs- HiValueListcontaining- 1,- 2,- 3
- range(5, 10.3)evaluates to- [5, 6, 7, 8, 9, 10]
- fold(add, [11, 22, 33])evaluates to- 66
- fold(mul, [11, 22, 33])evaluates to- 7986
- fold(div, [11, 22, 33])evaluates to- 1/66(left fold)
 Then overload existing operations to work on lists: - length([1, true, "Hello"])evaluates to- 3
- reverse([1, true, "Hello"])evaluates to- ["Hello", true, 1]
- [1, 2] + [3, 4, 5]evaluates to- [1, 2, 3, 4, 5]
- [0, "x"] * 3evaluates to- [0, "x", 0, "x", 0, "x"](tip: use- stimes)
 When a list is used as a function, perform indexing/slicing: - ["hello", true, "world"](1)evaluates to- true
- ["hello", true, "world"](1,3)evaluates to- [true, "world"]
 
The following session in the REPL should be possible:
hi> list(1, 2, 3, 4, 5)
[ 1, 2, 3, 4, 5 ]
hi> fold(add, [2, 5] * 3)
21
hi> fold(mul, range(1, 10))
3628800
hi> [0, true, false, "hello", "world"](2, 4)
[ false, "hello" ]
hi> reverse(range(0.5, 70/8))
[ 8.5, 7.5, 6.5, 5.5, 4.5, 3.5, 2.5, 1.5, 0.5 ]
Lists of bytes (numbers from 0 to 255) can be represented and processed more efficiently. Let us introduce a new value type for them.
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunPackBytes | HiFunUnpackBytes | HiFunEncodeUtf8 | HiFunDecodeUtf8 | HiFunZip | HiFunUnzip | HiFunSerialise | HiFunDeserialise data HiValue = ... | HiValueBytes ByteString Bytes are represented using the strict ByteStringtype from thebytestringpackage.
- 
In the parser, add support for the following constructs: - Built-in names pack-bytes,unpack-bytes,zip,unzip,encode-utf8,decode-utf8,serialise, anddeserialise, that are parsed into the correspondingHiFunconstructors.
- Byte array literals, such as [# 01 3f ec #]that are parsed intoHiValueBytes. Each element is a two-digit hexadecimal number.
 
- Built-in names 
- 
In the evaluator, implement the new operations: - pack-bytes([ 3, 255, 158, 32 ])evaluates to- [# 03 ff 9e 20 #]
- unpack-bytes([# 10 20 30 #])evaluates to- [16, 32, 48]
- encode-utf8("Hello!")evaluates to- [# 48 65 6c 6c 6f 21 #]
- decode-utf8([# 48 65 6c 6c 6f #])evaluates to- "Hello"
- decode-utf8([# c3 28 #])evaluates to- null(invalid UTF-8 byte sequence)
- zipcompresses the bytes using the- zlibpackage (specify- bestCompression)
- serialiseturns any value into bytes using the- serialisepackage
- for all A,unzip(zip(A)) ≡ Aholds
- for all A,deserialise(serialise(A)) ≡ Aholds
 Then overload existing operations to work on bytes: - [# 00 ff #] + [# 01 e3 #]evaluates to- [# 00 ff 01 e3 #]
- [# 00 ff #] * 3evaluates to- [# 00 ff 00 ff 00 ff #](tip: use- stimes)
 When bytes are used as a function, perform indexing/slicing as with strings and lists: - [# 00 ff 01 e3 #](1)evaluates to- 255
- [# 00 ff 01 e3 #](1,3)evaluates to- [# ff 01 #]
 
The following session in the REPL should be possible:
hi> pack-bytes(range(30, 40))
[# 1e 1f 20 21 22 23 24 25 26 27 28 #]
hi> zip(encode-utf8("Hello, World!" * 1000))
[# 78 da ed c7 31 0d 00 20 0c 00 30 2b f0 23 64 0e 30 00 df 92 25 f3 7f a0 82 af
   fd 1a 37 b3 d6 d8 d5 79 66 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88
   88 88 88 88 88 88 88 88 fc c9 03 ca 0f 3b 28 #]
hi> decode-utf8([# 68 69 #] * 5)
"hihihihihi"
hi> unzip([# 78 da 63 64 62 06 00 00 0d 00 07 #])
[# 01 02 03 #]
In this task we extend the language with I/O capabilities. We consider it to be the most important part of the homework and it is graded with extra points.
Let us start by creating a new type in Hi.Base that encodes the available actions:
data HiAction =
    HiActionRead  FilePath
  | HiActionWrite FilePath ByteString
  | HiActionMkDir FilePath
  | HiActionChDir FilePath
  | HiActionCwdNow recall that the type of our eval function is as follows:
eval :: Monad m => HiExpr -> m (Either HiError HiValue)We could just require m to be IO in order to execute the actions, but that would be bad design, as it would make it impossible to do evaluation in a pure, deterministic context (e.g. for tests). Instead, let us create a new class in Hi.Base:
class Monad m => HiMonad m where
  runAction :: HiAction -> m HiValueOne could imagine at least a few possible instances of this class:
- IO, where those actions could interact with the actual file system
- Identity, where the actions do nothing and return- null
- State FS, where- FSis a pure and deterministic in-memory simulation of the file system
- ReaderT Permissions IO, which can be more secure than- IOby controlling whether the program has read-only or read-write access
While it would be useful to implement all of those, we shall limit ourselves to the last one to avoid making the task unnecessarily tedious.
In a new module Hi.Action, declare the following:
data HiPermission =
    AllowRead
  | AllowWrite
data PermissionException =
  PermissionRequired HiPermission
instance Exception PermissionException
newtype HIO a = HIO { runHIO :: Set HiPermission -> IO a }Finally, let us change the type of eval as follows:
- eval ::   Monad m => HiExpr -> m (Either HiError HiValue)
+ eval :: HiMonad m => HiExpr -> m (Either HiError HiValue)With those preliminaries out of the way, we can start integrating the actions into the rest of the language.
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunRead | HiFunWrite | HiFunMkDir | HiFunChDir data HiValue = ... | HiValueAction HiAction data HiExpr = ... | HiExprRun HiExpr 
- 
In the parser, add support for the following constructs: - Built-in names read,write,mkdir,cd, that are parsed into the correspondingHiFunconstructors.
- Built-in name cwdthat is parsed intoHiValueActionHiActionCwd.
- E!notation that is parsed into- HiExprRun, e.g.- read("hello.txt")!,- mkdir("projects")!, or- cwd!.
 
- Built-in names 
- 
In the evaluator, implement the new functions to return the corresponding actions: - read("hi.txt")evaluates to- read("hi.txt"). While visually the same, internally the first one is HiExprApply and the second one is HiValueAction.
- write("hi.txt", "Hi!")evaluates to- write("hi.txt", [# 48 69 21 #])
- mkdir("dir")evaluates to- mkdir("dir")
- cd("dir")evaluates to- cd("dir")
 
Then implement the HiExprRun construct, which should execute the action using runAction that we defined earlier.
- 
Define the HiMonad HIOinstance, such that:- cwd!returns the current working directory
- cd("mydir")!changes the current working directory to- mydir
- read("myfile")!returns the contents of- myfile(use- HiValueStringif the contents are valid UTF-8 and- HiValueBytesotherwise)
- read("mydir")!returns the directory listing of- mydir
- write("myfile", "Hello")!writes- "Hello"to- myfile
- mkdir("mydir")!creates a new directory- mydir
 Use the directorypackage to implement all of the above.
- 
Implement permission control in HiMonad HIO, so that actions throwPermissionException(usingthrowIO) unless they are allowed.- AllowReadenables- cwd,- cd,- read
- AllowWriteenables- write,- mkdir
 
The following session in the REPL should be possible:
hi> mkdir("tmp")!
null
hi> read("tmp")!
[]
hi> mkdir("tmp/a")!
null
hi> mkdir("tmp/b")!
null
hi> read("tmp")!
[ "a", "b" ]
hi> write("tmp/hi.txt", "Hello")!
null
hi> cd("tmp")!
null
hi> read("hi.txt")!
"Hello"
Note that actions are just values and only ! forces their execution:
hi> read
read
hi> read("hi.txt")
read("hi.txt")
hi> read("hi.txt")!
"Hello"
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunParseTime data HiValue = ... | HiValueTime UTCTime data HiAction = ... | HiActionNow Time is represented using the UTCTimetype from thetimepackage.Extend the data types in Hi.Actionto include the following constructors:data HiPermission = ... | AllowTime 
- 
In the parser, add support for the following constructs: - Built-in name parse-timethat is parsed into the correspondingHiFunconstructor.
- Built-in name nowthat is parsed into the correspondingHiActionconstructor.
 
- Built-in name 
- 
In the evaluator, implement parse-timeusingreadMaybeto parse aHiValueStringinto aHiValueTime, orHiValueNullin case of failure.
- 
In the HiMonad HIOinstance, implement theHiActionNowto return the current system time. It requires theAllowTimepermission.
- 
In the evaluator, overload existing operations to work on time: - parse-time("2021-12-15 00:00:00 UTC") + 1000evaluates to- parse-time("2021-12-15 00:16:40 UTC")(use- addUTCTime)
- parse-time("2021-12-15 00:37:51.000890793 UTC") - parse-time("2021-12-15 00:37:47.649047038 UTC")evaluates to- 3.351843755(use- diffUTCTime)
 
The following session in the REPL should be possible:
hi> now!
parse-time("2021-12-15 00:42:33.02949461 UTC")
hi> parse-time("2021-01-01 00:00:00 UTC") + 365 * 24 * 60 * 60
parse-time("2022-01-01 00:00:00 UTC")
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunRand data HiAction = ... | HiActionRand Int Int 
- 
In the parser, add support for the built-in name randthat is parsed into the correspondingHiFunconstructor.
- 
In the evaluator, implement the new function: - rand(0, 10)evaluates to- rand(0, 10). While visually the same, internally the first one is- HiExprApplyand the second one is- HiValueAction.
 
- 
Extend the HiMonad HIOinstance, so that:- rand(0, 5)!evaluates to- 0,- 1,- 2,- 3,- 4, or- 5
- the distribution of random numbers is uniform
Tip: use the randompackage.
 
The following session in the REPL should be possible:
hi> rand
rand
hi> rand(0, 10)
rand( 0, 10 )
hi> rand(0, 10)!
8
hi> rand(0, 10)!
3
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunEcho data HiAction = ... | HiActionEcho Text 
- 
In the parser, add support for the built-in name echothat is parsed into the correspondingHiFunconstructor.
- 
In the evaluator, implement the new function: - echo("Hello")evaluates to- echo("Hello"). While visually the same, internally the first one is- HiExprApplyand the second one is- HiValueAction.
 
- 
Extend the HiMonad HIOinstance, so thatecho("Hello")!printsHellofollowed by a newline tostdout. It requires theAllowWritepermission.
- 
In the evaluator, ensure that if(true, A, B)does not evaluateB, andif(false, A, B)does not evaluateA.Then generalise A && Bas follows:- if Aisfalseornull, returnAwithout evaluatingB
- otherwise, evaluate and return B
 Generalise A || Bas follows:- if Aisfalseornull, evaluate and returnB
- otherwise, return Awithout evaluatingB
 
- if 
The following session in the REPL should be possible:
hi> echo
echo
hi> echo("Hello")
echo("Hello")
hi> echo("Hello")!
Hello
null
hi> "Hello"(0) || "Z"
"H"
hi> "Hello"(99) || "Z"
"Z"
hi> if(2 == 2, echo("OK")!, echo("WTF")!)
OK
null
hi> true || echo("Don't do this")!
true
hi> false && echo("Don't do this")!
false
hi> [# 00 ff #] && echo("Just do it")!
Just do it
null
- 
Extend the data types in Hi.Baseto include the following constructors:data HiFun = ... | HiFunCount | HiFunKeys | HiFunValues | HiFunInvert data HiValue = ... | HiValueDict (Map HiValue HiValue) data HiExpr = ... | HiExprDict [(HiExpr, HiExpr)] Dictionaries are represented using the Maptype from thecontainerspackage.
- 
In the parser, add support for the following constructs: - Built-in names count,keys,values,invert, that are parsed into the correspondingHiFunconstructors.
- Dictionary literals, written as { I: A, J: B, K: C }, for example:- { "width": 120, "height": 80 }
- { 1: true, 3: true, 4: false }
 
- Dot access, written as E.fld, that is parsed into function applicationE("fld"). For example, the following holds:{ "width": 120, "height": 80 }.width ≡ { "width": 120, "height": 80 }("width")
 
- Built-in names 
- 
In the evaluator, implement the new operations: - { "width": 120, "height": 80 }("width")evaluates to- 120
- keys({ "width": 120, "height": 80 })evaluates to- ["height", "width"](sorted)
- values({ "width": 120, "height": 80 })evaluates to- [80, 120](sorted by key)
- count("XXXOX")evaluates to- { "O": 1, "X": 4 }
- count([# 58 58 58 4f 58 #])evaluates to- { 79: 1, 88: 4 }
- count([true, true, false, true])evaluates to- { false: 1, true: 3 }
- invert({ "x": 1, "y" : 2, "z": 1 })evaluates to- { 1: [ "z", "x" ], 2: ["y"] }
 
The following session in the REPL should be possible:
hi> count("Hello World").o
2
hi> invert(count("big blue bag"))
{ 1: [ "u", "l", "i", "e", "a" ], 2: [ "g", " " ], 3: ["b"] }
hi> fold(add, values(count("Hello, World!")))
13