CS442 Assignment 3 Winter 2023
Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit
CS442
Assignment 3
Winter 2023
This assignment tests your understanding of the content of Module 5. You will implement a functional language in GNU Smalltalk. As Assignment 2 was also the implementation of a functional language, you will find it to be a useful starting place. This assignment consists of two parts: The implementation of a functional language, and the implementation of a monad.
In this and all assignments, any behavior which we do not explicitly define will not be tested, so you may define it however you wish, or allow your program to fail. Make sure though that it actually is undefined; ask on Piazza if you’re unsure.
Submit all code via UWaterloo submit, e.g.:
submit cs442 a3 .
This assignment is due on Friday, March 10th, by 12PM NOON, NOT MIDNIGHT, Eastern time.
Build You a Haskell
You are provided with hasntkell .st, an implementation of the types for the syntax tree of a (severely) reduced version of Haskell, called Hasn’t Kell, or Hasntkell for short. Hasn’t Kell is defined by the syntax and semantics in the appendix to Module 5, with the following syntactic sugar, simplifications, and additions:
• I/O is supported through the IO monad. IO monad objects are implemented as Smalltalk objects. Abstrac- tions which evaluate to IO monad objects when applied are also implemented as Smalltalk objects.
• Let bindings are not part of the parsed syntax. Instead, they are simply replaced by abstractions and applications. For instance, let x = y in z is rewritten as (^x .z) y .
• Algebraic datatypes and matching are not implemented.
• A simple sort of tuple is supported as syntactic sugar, by replacing an expression such as [3 1 4 1 ] with a function which, given a value n, evaluates to the nth element of the tuple. For instance, [3 1 4 1 ] 1 is 3,
[3 1 4 ] 2 is 1. If n = 0, the length of the tuple is returned, so [3 1 ] 0 is 2. If n is too large, error is returned.
• Division is removed and replaced with an equality operator, = .
• A subtraction x − y where x < y should evaluate to 0, rather than getting stuck. This was our alternate definition of subtraction over natural numbers.
hasntkell .st implements the following classes (and extension to the String class):
Object subclass: HasntkellExp [
isVar .
isAbs .
isApp .
isBoolLiteral .
isTrue .
isFalse .
isIf .
isNum .
isNumExp .
isError .
isIOAbs .
isIO .
isValue .
reduceWith: block .
freeVars .
freeVars: map .
printString .
]
HasntkellExp subclass: HasntkellVar [
HasntkellVar class >> withName: name .
dup .
isVar .
name .
freeVars: map .
displayString .
]
HasntkellExp subclass: HasntkellAbs [
HasntkellAbs class >> withVar: var body: body .
dup .
isAbs .
var .
body .
freeVars: map .
displayString .
]
HasntkellExp subclass: HasntkellApp [
HasntkellApp class >> withRator: rator rand: rand .
dup .
isApp .
rator .
rand .
freeVars: map .
displayString .
]
HasntkellExp subclass: HasntkellBoolLiteral [
isBoolLiteral .
isValue .
]
HasntkellBoolLiteral subclass: HasntkellTrue [
value .
dup .
isTrue .
displayString .
]
HasntkellBoolLiteral subclass: HasntkellFalse [
value .
dup .
isFalse .
displayString .
]
HasntkellExp subclass: HasntkellIf [
HasntkellIf class >> withCondition: scond thenExp: sthene elseExp: selsee .
dup .
isIf .
condition .
thenExp .
elseExp .
freeVars: map .
displayString .
]
HasntkellExp subclass: HasntkellNum [
HasntkellNum class >> withValue: value .
dup .
isNum .
isValue .
value .
displayString .
]
HasntkellExp subclass: HasntkellNumExp [
HasntkellNumExp class >> withOp: sop left: sleft right: sright .
dup .
isNumExp .
op .
left .
right .
displayString .
]
HasntkellExp subclass: HasntkellError [
dup .
isError .
isValue .
displayString .
]
HasntkellExp subclass: HasntkellIOAbs [
HasntkellIOAbs class >> withConstructor: scons .
dup .
isIOAbstraction .
apply: exp .
displayString .
]
HasntkellExp subclass: HasntkellIO [
isIO .
displayString .
]
HasntkellIO subclass: HasntkellReadNum [
dup .
performIO: globals heap: heap .
]
HasntkellIO subclass: HasntkellWriteNum [
HasntkellWriteNum class >> new: sexp .
dup .
performIO: globals heap: heap .
]
HasntkellIOAbs subclass: HasntkellCurryBinding [
HasntkellCurryBinding class >> new: sleft .
dup .
apply: exp .
]
HasntkellIO subclass: HasntkellBinding [
HasntkellBinding class >> withLeft: sleft right: sright .
dup .
performIO: globals heap: heap .
]
Object subclass: HasntkellParser [
HasntkellParser class >> new .
HasntkellParser class >> new: text .
HasntkellParser class >> parse: text .
HasntkellParser class >> parseFile: text withGlobals: globals .
parseFile: globals .
parse .
]
Object subclass: HasntkellPrelude [
HasntkellPrelude class >> globals .
]
String extend [
runHasntkell .
]
The HasntkellExp , HasntkellVar , HasntkellAbs , and HasntkellApp classes act exactly like the LambdaExp family of classes from Assignment 2. The other subclasses of HasntkellExp are new types of expressions in Hasn’t Kell. New is* methods have been added for all the new subclasses, and the HasntkellExp>>reduceWith: method has had its steps: argument removed, as we will only be performing full reductions in this assignment. In addition, the isValue method has been added, which returns true in all classes representing terminal values.
The HasntkellBoolLiteral class represents boolean literals, with its two subclasses HasntkellTrue and Has -
ntkellFalse .
The HasntkellIf class represents an if -then -else expression. Its components can be extracted by the condition , thenExp , and elseExp methods.
The HasntkellNum class represents a numeric value. Its value can be extracted with value .
The HasntkellNumExp class represents a binary numeric expression. Its operator can be extracted as a string with op, and will always be one of '+ ' , ' - ' , '* ' , or '= ' . Its operands can be extracted with left and right . Remember that numbers, per the semantics, are all natural numbers (integers greater than or equal to zero).
The HasntkellError class represents an error. Per the semantics, an error has no error message, and so nothing can be extracted from a HasntkellError , except of course for the fact that it is an error.
The HasntkellIOAbs class represents an IO abstraction, such as Haskell’s putStrLn . This has to be a separate kind of abstraction from HasntkellAbs because the IO monad is a black box, in which you can perform no substitution. The apply: method is used to perform application on the HasntkellIOAbs with the given expression, returning a
HasntkellIO .
The HasntkellIO class represents an IO monad. It is essentially a black box, but its performIO:heap: method performs the I/O, given a dictionary of globals (σ) and a heap dictionary (Σ) as arguments. Note that none of the built-in IO monads use the heap argument; you will use it in your own monad.
The HasntkellReadNum and HasntkellWriteNum classes represent our two IO monads, which read and write numbers as lines.
The HasntkellCurryBinding and HasntkellBinding methods represent the single-argument curried and complete forms of a bound pair of IO monads.
The HasntkellParser class is like LambdaParser, but with extended syntax for the new features of Hasn’t Kell. As well as all the methods that LambdaParser had, because Hasntkell has a file syntax with declarations, it additionally implements HasntkellParser class>>parseFile:withGlobals: and parseFile: , both of which take a dictionary of pre-declared declarations (the standard library) and return the same dictionary with the declarations in the given
file added. For instance, you can parse the file 'main = factorial 2 ; factorial = ^x . if (= x 0 ) then 1 else * x (factorial ( - x 1 ) ) ' in either of these two ways, given a pre-existing variable globals:
f := 'main = factorial 2 ; factorial = ^x . if (= x 0 ) then 1 else * x (factorial ( - x 1 ) ) ' . p := HasntkellParser new: f .
e := p parseFile: globals .
...
e := HasntkellParser parseFile: f withGlobals: globals .
The HasntkellPrelude class represents the standard library, i.e., Prelude . The HasntkellPrelude class>>globals method returns a dictionary of pre-defined global variables; read its implementation to see what they are. This is a class method, so it’s called simply with HasntkellPrelude globals .
Finally, the String class itself is extended with a runHasntkell method, such that (once your assignment is complete), you can run Hasntkell code by simply calling that method on a string containing Hasntkell code.
Several examples are included in the “demonstration” section of this document.
Note that we will only be testing code which evaluates to a number, boolean, error, or IO monad, or which gets stuck, and will not be testing any code that evaluates to an abstraction, so variable naming is not a concern. This is so that you can internally use de Bruijn indices or not, or rename variables as you please, or, frankly, perform reduction in any (correct) way you wish.
1 NOE reduction
Create a file, a3 .st, which implements at least the following class and method:
Object subclass: Hasntkell [
Hasntkell class >> new: exp withGlobals: dict .
eval .
]
You can (and should!) start this by using the Lambda class you implemented in Assignment 2. Hasntkell’s reductions are based on NOR’s simplified form NOE, so you can use the nor and nor: methods as a starting point.
The eval method fully evaluates the expression stored in the Hasntkell with the global variables (σ) given by dict . If the expression reduces forever, eval should never return (i.e., don’t try to solve the halting problem). Note that since Hasn’t Kell does not include let bindings, σ never changes. Make sure you do not reduce inside an abstraction, or reduce the rand of an application, as those are standard in NOR but not done in Haskell. It is invalid to call eval twice on the same Hasntkell or expression, so you may update the expression in any way you please.
Be careful to use dup when appropriate. Just like in Assignment 2, if you mutate the expression, you will need to be careful to dup during substitution so that you don’t have multiple references to the same mutable expression. In this case, as extracting variables from the store is similar to substitution, you will have to be careful to dup there as well if you mutate expressions.
Since there is no method to take a single step of reduction, you cannot and will not be judged on whether your individual reduction steps work as expected. If you wish to implement reduction in a very different way, you are free to. Just make sure you use lazy evaluation, as expressions which terminate under NOR but reduce forever under AOR or AOE will absolutely be tested. In addition, “getting stuck”behavior will be tested, including but not limited to:
• If the condition of an if -then -else does not evaluate to a boolean, then the reduction should get stuck.
• If a numeric binary expression is used and either of the operands does not reduce to a number, then the reduction should get stuck. Note that this is also true of the added = operator, which should only check equality of numbers, not arbitrary expressions.
• If the rator of an application does not reduce to a HasntkellAbs or a HasntkellIOAbs , then the reduction should get stuck.
Remember to implement semantics for every non-terminal Hasntkell* class. In a HasntkellApp in which the rator is a HasntkellIOAbs (i.e., its isIOAbs returns true), the correct semantics is to reduce to the result of rator apply:
rand . Just like any other application, do not reduce the rand before calling apply: . Subclasses of HasntkellIO are terminal values, and cannot be reduced; do not try to use performIO: as reduction!
Be careful about types in your reduction. Reduction should never result in a value that is not a HasntkellExp . For instance, when adding two numbers, you should create a HasntkellNum with the actual result as its value, not just a number. When performing an equality comparison, you should create a HasntkellTrue or HasntkellFalse , not just true or false .
2 State Monad
Extend a3 .st, adding the following class:
Object subclass: HasntkellRefLib [
HasntkellRefLib class >> globals: dict .
]
The HasntkellRefLib class>>globals: method should extend the dictionary given as an argument with entries ' ref ' , 'get ' , and 'put ' , which implement the state monad as IO monads. ' ref ' and 'get ' should be abstractions (presumably HasntkellIOAbs values) which evaluate to IO monads of your own creation; you may want to look at the implementation of HasntkellWriteNum and 'writeNum ' for reference. 'put ' should be an abstraction which, when given two arguments, evaluates to an IO monad of your own creation; you may want to look at the implementation of HasntkellCurryBinding , HasntkellBinding , and 'bind ' for reference.
Note that since Hasntkell is lazily evaluated, the arguments to each of these will be stored in the IO monad unevaluated. It is up to each monad implementation to actually evaluate them.
The ' ref ' monad’s performIO:heap: method should fully evaluate its argument, then create a new entry in the heap dictionary, mapping a fresh label to that value. Its return value should be a Hasntkell value associated with the label. Exactly how you implement labels is up to you; perhaps labels are simply numbers, perhaps they are variables, perhaps they are a new subclass of HasntkellExp . The important thing is that labels are unique. In addition, as you are writing the only component that cares about the heap, you are free to use other keys in the heap to store information needed to create fresh labels.
The 'get ' monad’s performIO:heap: method should fully evaluate its argument, interpret it as a label, and return the value associated with that label in the heap. No error checking is required, so any behavior is allowed if the argument is not a label, or the label does not exist in the heap.
The 'put ' monad’s performIO:heap: method should fully evaluate both of its arguments, and interpret the first as a label and the second as a value. The heap should be updated such that the value associated with the label is replaced with the value given as a second argument. The return is the Hasntkell value associated with the label, not the value. GR: Fix this to use ℓ and v to be less confusing.
Hints
To implement this, you will need to implement several other classes. Those classes will not be tested directly, only via their behavior in the globals created by HasntkellRefLib . A suggested, but not required, design for those classes is:
HasntkellIO subclass: HasntkellRef [
HasntkellRef class >> new: sexp .
dup .
performIO: globals heap: heap .
]
HasntkellIO subclass: HasntkellGet [
HasntkellGet class >> new: sexp .
dup .
performIO: globals heap: heap .
]
HasntkellIOAbs subclass: HasntkellCurryPut [
HasntkellCurryPut class >> new: sleft .
dup .
apply: exp .
]
HasntkellIO subclass: HasntkellPut [
HasntkellPut class >> withLeft: sleft right: sright .
dup .
performIO: globals heap: heap .
]
Demonstration
We will demonstrate the various features step-by-step. If you’ve partially implemented Hasntkell>>eval but not HasntkellRefLib class>>globals:, you can stub out the latter with a trivial non-implementation:
Object subclass: HasntkellRefLib [
HasntkellRefLib class >> globals: dict [
^dict
]
]
which of course won’t work, but will allow String>>runHasntkell to operate.
Basics
Hasn’t Kell is the λ-calculus at its heart, and so anything you could do in the λ-calculus, you can do in Hasntkell, as well as recursion without the Y combinator. Note that we will not be testing examples of this type, as we won’t be testing abstractions for equality (so that you can continue to use de Bruijn indices or any form of substitution); still, λ-calculus expressions should behave in a predictable way, albeit with less reduction than nor:
st> 'two = ^f . ^x . f (f x); three = ^f . ^x . f (f (f x)) ; mul = ^m . ^n . ^f . m (n f); main = mul two three
' runHasntkell
(^f . (two (three f)))
st> 'main = (^x . x) (^y . y) ' runHasntkell
(^y .y)
None of this is interestingly changed from Assignment 2, so we will focus on other features.
Globals
The λ-calculus has no direct reduction for variables, but Hasntkell does. If the variable is present in the global scope, then the Variable rule applies:
st> 'fortytwo = 42; main = fortytwo ' runHasntkell
42
st> 'main = fortytwo ' runHasntkell
fortytwo
Be careful about evaluation order. If you replace variables too early, your result will be incorrect:
st> 'fortytwo = 14; main = (^fortytwo . fortytwo) 42 ' runHasntkell
42
" This would have returned 14 if we 'd replaced fortytwo at the wrong time "
Numbers and Math
Hasntkell should implement natural numbers and several operators. Just like Haskell, we can use this to convert Church numerals into natural numbers:
st> 'two = ^f . ^x . f (f x); three = ^f . ^x . f (f (f x)) ; mul = ^m . ^n . ^f . m (n f); main = mul two three (^x . (+ x 1 ) ) 0 ' runHasntkell
6
Of course, this could be done without Church numerals:
st> 'main = * 2 3 ' runHasntkell
6
Subtraction operates within the natural numbers:
st> 'main = - 2 3 ' runHasntkell
0
st> 'main = - 3 2 ' runHasntkell
1
And of course, all of it uses λ-calculus-like application syntax, rather than Haskell-like infix operator syntax (i.e., the operator doesn’t go in between its operands).
Booleans, Conditions, and Recursion
While it’s possible to implement λ-calculus-style booleans, Hasntkell also supports native booleans:
st> 'main = true ' runHasntkell
true
st> 'main = false ' runHasntkell
false
As well as numeric comparison:
st> 'main = = 1 1 ' runHasntkell
true
st> 'main = = 1 2 ' runHasntkell
false
st> 'main = = 1 ( - 2 1 ) ' runHasntkell
true
With the addition of if -then -else, it’s easy to write recursive functions:
st> 'factorial = ^x . if (= x 0 ) then 1 else * x (factorial ( - x 1 ) ) ; main = factorial 12 ' runHasntkell
479001600
You can store your numbers as Smalltalk integers, which are 64-bit on 64-bit systems, so don’t be concerned about overflow:
st> 'factorial = ^x . if (= x 0 ) then 1 else * x (factorial ( - x 1 ) ) ; main = factorial 21 ' runHasntkell
-4249290049419214848
Several other numeric comparisons which evaluate to booleans are implemented in HasntkellPrelude , but they are simply implemented in terms of = :
st> 'isFiveToTen = ^x . if (le x 10) then (ge x 5 ) else false; main = isFiveToTen 5 ' runHasntkell
true
st> 'isFiveToTen = ^x . if (le x 10) then (ge x 5 ) else false; main = isFiveToTen 11 ' runHasntkell
false
Errors
Hasn’t Kell errors—if the whole language isn’t an error—are pervasive, and should persist past all obstacles. In actuality, though, they will only be tested as rators or rands of applications:
st> 'main = error 42 ' runHasntkell
error
st> 'infinite = ^x . infinite (+ x 1 ) ; main = infinite error ' runHasntkell
error
Syntactic Sugar
Let bindings and tuples are just syntactic sugar. You do not need to do anything directly to implement them:
st> 'main = let x = 42 in x ' runHasntkell
42
st> 'main = [ 10 21 42] 3 ' runHasntkell
42
IO
If main resolves to an IO monad, then runHasntkell will call performIO:heap: on that IO monad, thus performing I/O:
st> 'main = writeNum 42 ' runHasntkell
42
42
Note that in the above example, 42 is written twice because the first is the behavior of the writeNum monad, and the second is the behavior of the GNU Smalltalk REPL. Monadic binding is done through the bind function, which behaves like >>= :
st> 'main = bind readNum (^x . writeNum (+ x 30) ) ' runHasntkell
12
42
42
In this case, the 12 is the keyboard input, the first 42 is the behavior of writeNum , and the third is the behavior of the GNU Smalltalk REPL. As the result of binding is itself an IO monad, further bindings can be performed, creating arbitrarily long chains of monads:
st> 'echo = bind readNum (^x . writeNum (+ x 30) ) ; main = bind echo (^x . echo) ' runHasntkell
1
31
2
32
32
There is no unsafePerformIO .
State
State is stored with references and the get and put monads. Here is an example that repeatedly decrements the value in a reference until it reaches zero:
st> 'findZeroBadly = ^ r . bind (get r) ^x . if (= x 0 ) then (writeNum x) else bind (put r ( - x 1 ) ) ^ig .
findZeroBadly r ; main = bind ( ref 42000) ^ r . findZeroBadly r ' runHasntkell
0
0
This example may take a moment (and the garbage collector may holler a lot), but if your implementation of NOE is correct, it should be impossible to overflow the stack with this kind of code.
Here is the example code, written with clearer indentation:
findZeroBadly =
^r .
bind (get r) ^x .
if (= x 0 ) then
(writeNum x)
else
bind (put r ( - x 1 )) ^ig .
findZeroBadly r;
main =
bind (ref 42) ^r .
findZeroBadly r
Here is an example which computes the factorial of a number, with both the number and the result in references:
st> ' refFac = ^i . ^o . bind (get i) ^x . bind (get o) ^y . if (= x 0 ) then writeNum y else bind (put i ( - x 1 ) ) ^ig . bind (put o (* x y)) ^ig . refFac i o ; main = bind ( ref 10) ^i . bind ( ref 1 ) ( refFac i) '
runHasntkell
3628800
3628800
And, the example code with clearer indentation:
refFac =
^i . ^o .
bind (get i) ^x .
bind (get o) ^y .
if (= x 0 ) then
writeNum y
else
bind (put i ( - x 1 )) ^ig .
bind (put o (* x y)) ^ig .
refFac i o;
main =
bind (ref 10) ^i .
bind (ref 1) (refFac i)
Finally, to put it all together, here is a program which takes a number as input, and then as many times as that number specifies, takes other numbers as inputs, and prints the factorial of those numbers:
st> ' refFac = ^i . ^o . bind (get i) ^x . bind (get o) ^y . if (= x 0 ) then writeNum y else bind (put i ( - x
1 ) ) ^ig . bind (put o (* x y)) ^ig . refFac i o ; refFacIn = bind readNum ^i . bind ( ref i) ^i . bind ( ref 1 ) ^o . refFac i o ; refFacN = ^i . if (le i 1 ) then refFacIn else bind refFacIn ^ig . ( refFacN ( - i 1 ) ) ; main = bind readNum refFacN ' runHasntkell
3
1
1
5
120
20
2432902008176640000
2432902008176640000
And, its clearer version:
refFac =
^i . ^o .
bind (get i) ^x .
bind (get o) ^y .
if (= x 0 ) then
writeNum y
else
bind (put i ( - x 1 )) ^ig .
bind (put o (* x y)) ^ig .
refFac i o;
refFacIn =
bind readNum ^i .
2023-02-23