Thanks @int-index for such a great homework.
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:
HW3.Base
HW3.Parser
HW3.Pretty
HW3.Evaluator
You are allowed to add more modules to the project, but those are the required ones.
-
In
HW3.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
HW3.Parser
, define the following function:parse :: String -> Either (ParseErrorBundle String Void) HiExpr
The
ParseErrorBundle
type comes from themegaparsec
package which we will use to implement our parser. -
In
HW3.Pretty
, define the following function:prettyValue :: HiValue -> Doc AnsiStyle
The
Doc
andAnsiStyle
types come from theprettyprinter
andprettyprinter-ansi-terminal
packages 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
HW3.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
HW3.Base
to 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
Rational
type fromData.Ratio
. -
In the parser, add support for the following constructs:
-
Built-in names
div
,mul
,add
, andsub
, that are parsed into the correspondingHiFun
constructors (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)) ]
-
-
In the evaluator, implement the arithmetic operations:
add(500, 12)
evaluates to512
(addition)sub(10, 100)
evaluates to-90
(subtraction)mul(23, 768)
evaluates to17664
(multiplication)div(57, 190)
evaluates to0.3
(division)
Nested function applications are allowed:
div(add(mul(2, 5), 1), sub(11,6))
evaluates to2.2
The following errors must be returned as
HiError
:HiErrorArityMismatch
: functions called with an incorrect amount of arguments, e.g.sub(1)
orsub(1, 2, 3)
.HiErrorDivideByZero
: thediv
function is called with0
as its second argument, e.g.div(1, 0)
ordiv(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
ExceptT
monad transformer to propagateHiError
through 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:
quotRem
fromPrelude
fromRationalRepetendUnlimited
from thescientific
package
- 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
HW3.Base
to 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 correspondingHiFun
constructors. -
Built-in names
true
andfalse
that are parsed intoHiValueBool
.
-
-
In the evaluator, implement the new operations.
Boolean algebra:
not(true)
evaluates tofalse
(negation)and(true, false)
evaluates tofalse
(conjunction)or(true, false)
evaluates totrue
(disjunction)
Equality checking:
equals(10, 10)
evaluates totrue
equals(false, false)
evaluates totrue
equals(3, 10)
evaluates tofalse
equals(1, true)
evaluates tofalse
(no implicit cast)
Comparisons:
less-than(3, 10)
evaluates totrue
less-than(false, true)
evaluates totrue
less-than(false, 0)
evaluates totrue
(Bool
is less thanNumber
)
Complements:
- for all
A
B
,greater-than(A, B) ≡ less-than(B, A)
holds - for all
A
B
,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
A
B
,if(true, A, B) ≡ A
holds - for all
A
B
,if(false, A, B) ≡ B
holds
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 / B
parses todiv(A, B)
A * B
parses tomul(A, B)
A + B
parses toadd(A, B)
A - B
parses tosub(A, B)
A < B
parses toless-than(A, B)
A > B
parses togreater-than(A, B)
A >= B
parses tonot-less-than(A, B)
A <= B
parses tonot-greater-than(A, B)
A == B
parses toequals(A, B)
A /= B
parses tonot-equals(A, B)
A && B
parses toand(A, B)
A || B
parses toor(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
HW3.Base
to include the following constructors:data HiFun = ... | HiFunLength | HiFunToUpper | HiFunToLower | HiFunReverse | HiFunTrim data HiValue = ... | HiValueNull | HiValueString Text
Strings are represented using the
Text
type from thetext
package. -
In the parser, add support for the following constructs:
-
Bulit-in names
length
,to-upper
,to-lower
,reverse
,trim
, that are parsed into the correspondingHiFun
constructors. -
Built-in name
null
that is parsed intoHiValueNull
. -
String literals, such as
"hello"
,"42"
, or"header\nfooter"
, that are parsed intoHiValueString
(tip: useText.Megaparsec.Char.Lexer.charLiteral
).
-
-
In the evaluator, implement the new operations:
length("Hello World")
evaluates to11
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" * 5
evaluates to"CatCatCatCatCat"
(tip: usestimes
)"/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 tonull
"Hello World"(99)
evaluates tonull
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
HW3.Base
to include the following constructors:data HiFun = ... | HiFunList | HiFunRange | HiFunFold data HiValue = ... | HiValueList (Seq HiValue)
Lists are represented using the
Seq
type from thecontainers
package. -
In the parser, add support for the following constructs:
-
Built-in names
list
,range
,fold
, that are parsed into the correspondingHiFun
constructors. -
List literals, written as
[A, B, C, ...]
, that are parsed into function applicationlist(A, B, C, ...)
.
-
-
In the evaluator, implement the new operations:
list(1, 2, 3)
constructsHiValueList
containing1
,2
,3
range(5, 10.3)
evaluates to[5, 6, 7, 8, 9, 10]
fold(add, [11, 22, 33])
evaluates to66
fold(mul, [11, 22, 33])
evaluates to7986
fold(div, [11, 22, 33])
evaluates to1/66
(left fold)
Then overload existing operations to work on lists:
length([1, true, "Hello"])
evaluates to3
reverse([1, true, "Hello"])
evaluates to["Hello", true, 1]
[1, 2] + [3, 4, 5]
evaluates to[1, 2, 3, 4, 5]
[0, "x"] * 3
evaluates to[0, "x", 0, "x", 0, "x"]
(tip: usestimes
)
When a list is used as a function, perform indexing/slicing:
["hello", true, "world"](1)
evaluates totrue
["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
HW3.Base
to include the following constructors:data HiFun = ... | HiFunPackBytes | HiFunUnpackBytes | HiFunEncodeUtf8 | HiFunDecodeUtf8 | HiFunZip | HiFunUnzip | HiFunSerialise | HiFunDeserialise data HiValue = ... | HiValueBytes ByteString
Bytes are represented using the strict
ByteString
type from thebytestring
package. -
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 correspondingHiFun
constructors. -
Byte array literals, such as
[# 01 3f ec #]
that are parsed intoHiValueBytes
. Each element is a two-digit hexadecimal number.
-
-
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 tonull
(invalid UTF-8 byte sequence)zip
compresses the bytes using thezlib
package (specifybestCompression
)serialise
turns any value into bytes using theserialise
package- for all
A
,unzip(zip(A)) ≡ A
holds - for all
A
,deserialise(serialise(A)) ≡ A
holds
Then overload existing operations to work on bytes:
[# 00 ff #] + [# 01 e3 #]
evaluates to[# 00 ff 01 e3 #]
[# 00 ff #] * 3
evaluates to[# 00 ff 00 ff 00 ff #]
(tip: usestimes
)
When bytes are used as a function, perform indexing/slicing as with strings and lists:
[# 00 ff 01 e3 #](1)
evaluates to255
[# 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 HW3.Base
that encodes the available actions:
data HiAction =
HiActionRead FilePath
| HiActionWrite FilePath ByteString
| HiActionMkDir FilePath
| HiActionChDir FilePath
| HiActionCwd
Now 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 HW3.Base
:
class Monad m => HiMonad m where
runAction :: HiAction -> m HiValue
One 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 returnnull
-
State FS
, whereFS
is a pure and deterministic in-memory simulation of the file system -
ReaderT Permissions IO
, which can be more secure thanIO
by 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 HW3.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
HW3.Base
to 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 correspondingHiFun
constructors. -
Built-in name
cwd
that is parsed intoHiValueAction HiActionCwd
. -
E!
notation that is parsed intoHiExprRun
, e.g.read("hello.txt")!
,mkdir("projects")!
, orcwd!
.
-
-
In the evaluator, implement the new functions to return the corresponding actions:
read("hi.txt")
evaluates toread("hi.txt")
. While visually the same, internally the first one isHiExprApply
and the second one isHiValueAction
.write("hi.txt", "Hi!")
evaluates towrite("hi.txt", [# 48 69 21 #])
mkdir("dir")
evaluates tomkdir("dir")
cd("dir")
evaluates tocd("dir")
Then implement the
HiExprRun
construct, which should execute the action usingrunAction
that we defined earlier. -
Define the
HiMonad HIO
instance, such that:cwd!
returns the current working directorycd("mydir")!
changes the current working directory tomydir
read("myfile")!
returns the contents ofmyfile
(useHiValueString
if the contents are valid UTF-8 andHiValueBytes
otherwise)read("mydir")!
returns the directory listing ofmydir
write("myfile", "Hello")!
writes"Hello"
tomyfile
mkdir("mydir")!
creates a new directorymydir
Use the
directory
package to implement all of the above. -
Implement permission control in
HiMonad HIO
, so that actions throwPermissionException
(usingthrowIO
) unless they are allowed.AllowRead
enablescwd
,cd
,read
AllowWrite
enableswrite
,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
HW3.Base
to include the following constructors:data HiFun = ... | HiFunParseTime data HiValue = ... | HiValueTime UTCTime data HiAction = ... | HiActionNow
Time is represented using the
UTCTime
type from thetime
package.Extend the data types in
HW3.Action
to include the following constructors:data HiPermission = ... | AllowTime
-
In the parser, add support for the following constructs:
-
Built-in name
parse-time
that is parsed into the correspondingHiFun
constructor. -
Built-in name
now
that is parsed into the correspondingHiAction
constructor.
-
-
In the evaluator, implement
parse-time
usingreadMaybe
to parse aHiValueString
into aHiValueTime
, orHiValueNull
in case of failure. -
In the
HiMonad HIO
instance, implement theHiActionNow
to return the current system time. It requires theAllowTime
permission. -
In the evaluator, overload existing operations to work on time:
parse-time("2021-12-15 00:00:00 UTC") + 1000
evaluates toparse-time("2021-12-15 00:16:40 UTC")
(useaddUTCTime
)parse-time("2021-12-15 00:37:51.000890793 UTC") - parse-time("2021-12-15 00:37:47.649047038 UTC")
evaluates to3.351843755
(usediffUTCTime
)
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
HW3.Base
to include the following constructors:data HiFun = ... | HiFunRand data HiAction = ... | HiActionRand Int Int
-
In the parser, add support for the built-in name
rand
that is parsed into the correspondingHiFun
constructor. -
In the evaluator, implement the new function:
rand(0, 10)
evaluates torand(0, 10)
. While visually the same, internally the first one isHiExprApply
and the second one isHiValueAction
.
-
Extend the
HiMonad HIO
instance, so that:rand(0, 5)!
evaluates to0
,1
,2
,3
,4
, or5
- the distribution of random numbers is uniform
Tip: use the
random
package.
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
HW3.Base
to include the following constructors:data HiFun = ... | HiFunEcho data HiAction = ... | HiActionEcho Text
-
In the parser, add support for the built-in name
echo
that is parsed into the correspondingHiFun
constructor. -
In the evaluator, implement the new function:
echo("Hello")
evaluates toecho("Hello")
. While visually the same, internally the first one isHiExprApply
and the second one isHiValueAction
.
-
Extend the
HiMonad HIO
instance, so thatecho("Hello")!
printsHello
followed by a newline tostdout
. It requires theAllowWrite
permission. -
In the evaluator, ensure that
if(true, A, B)
does not evaluateB
, andif(false, A, B)
does not evaluateA
.Then generalise
A && B
as follows:- if
A
isfalse
ornull
, returnA
without evaluatingB
- otherwise, evaluate and return
B
Generalise
A || B
as follows:- if
A
isfalse
ornull
, evaluate and returnB
- otherwise, return
A
without 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
HW3.Base
to 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
Map
type from thecontainers
package. -
In the parser, add support for the following constructs:
-
Built-in names
count
,keys
,values
,invert
, that are parsed into the correspondingHiFun
constructors. -
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")
-
-
In the evaluator, implement the new operations:
{ "width": 120, "height": 80 }("width")
evaluates to120
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