CS558 Homework #7
Due 4:00 PM, Tuesday, March 4, 2014

Homework must be submitted via D2L. All submitted files (n for this assignment sol7A.hs) must be submitted in the appropriate D2L directory in the drop box HW7. It is your responsibility to submit the homework in the proper format with the proper names.

All programs mentioned can be downloaded from the this document

In this homework we consider a variant of simple language with user defined types and a simple module system we call E7. The language, E7, has the following features:

  1. A simple imperative language with assignment, write, blocks, and while loops, similar to language E3.
  2. Primitive types: integers, characters, booleans, and their operations, similar to language E4.
  3. First class functions, with anonymous lambda expressions similar to language E5.
  4. A polymorphic type system similar to language E6.
  5. The ability for the user to define new types. The familiar pairs and lists can now be defined by the user, but come predefined in the language prelude.
  6. A simple module system that allows programs to be spread across multiple files, and which checks the type consistency across files.

E7's concretized abstract syntax is given by the following grammar:

prog := module { globaldef | fundef | datadef | adtdef | sigdef | import }  'main' exp

typ := 'Int'
| 'Bool'
| 'Char'
| '(' Id {typ} ')'                          -- User defined types
| '(' typ '.' typ ')'                       -- syntax for (Pair x y)
| '[' typ ']'                               -- syntax for (List x)
| letter                                    -- polymorphic type variable

exp :=
  id                                        -- imperative features
| '(' ':=' id exp ')'
| '(' 'local' '(' { id exp } ')' exp ')'
| '(' ':=' exp exp ')'
| '(' 'write' exp ')'
| '(' 'block' { exp } ')'
| '(' 'while' exp exp ')'

| True                                      -- Booleans
| False
| '(' 'if' exp exp exp ')'

| int                                       -- Integers
| '(' '+' exp exp ')'
| '(' '-' exp exp ')'
| '(' '*' exp exp ')'
| '(' '/' exp exp ')'
| '(' '<=' exp exp ')'

| char                                      -- Characters
| string                                    -- familiar "abc" syntax for list of Char
| '(' = exp exp ')


| '(' '@' exp { exp } ')'                  -- function application
| '(' '\' ( {id} ) exp ')'                 -- lambda abstraction (1st class anonymous functions)


| '(' '#' id  {exp}  ')'                  -- data construction (like: cons,nil,pair)
| '(' '!' id  exp    ')'                  -- data selection (like: head,tail,fst)
| '(' '?' id int exp ')'                  -- data predicates (like: null)


globaldef := '('  'global' vname typ exp ')'

fundef := '(' 'fun' id typ '(' { id typ } ')' exp ')'

datadef := '(' data '('Id {id} ')' { '(' '#' id {typ} ')' } ')'

adtdef := '(' 'adt' '('Id {id} ')' typ { globaldef | fundef | datadef }  ')'

sigdef := '(' 'signature' Id {sigitem} ')'
        | '(' 'signature' string ')'      -- (signature "test.e7") read signature from a file

import := '(' 'import' string implements sigExp ')'
        | '(' 'import' string hiding '(' {Id | id } ')' ')'

module := '(' 'module' Id in sigExp out sigExp ')'

sigExp :=
  Id
| 'prelude'
| 'everything'
| '(' 'sig' { sigItem } ')'
| '(' hide' sigExp '(' {Id | id } ')' ')'
| '(' 'union' { sigExp } ')'
| '(' 'file' string ')'

sigItem :=
   '(' val id typ ')
   '(' data '('Id {id} ')' { '(' '#' id {typ} ')' } ')'
   '(' type '(' Id {typ} ')'  ')'

id := lower { lower | upper | digit }
Id := upper { lower | upper | digit }

Note that the syntax for expressions has been divided into groups, where each group supports one aspect of the language. The types for lists and pairs are no longer primitive. These types come predefined.

(data (List a) (#nil) (#cons a (List a)))

(data (Pair a b) (#pair a b))

A sample file, which implements the operations on lists and pairs (nil,cons,head,tail,null,pair,fst, and snd) and many other familiar functions and types is typedlists.e7.

The module System.

The biggest changes have to do with what constitutes a whole program (because of the module system). Let's look more closely at this, so consider the program simple.e7
(module Simple in prelude out everything)

(global ten Int 10)

(fun and Bool (x Bool y Bool) (if x y x))
(fun eq Bool (x Int y Int) (@and (<= x y) (<= y x)))

{ functions can be mutually recursive }
(fun even Bool (n Int) (if (@eq n 0) True (@ odd (- n 1))))

(fun odd Bool (n Int) (if (@eq n 0)  False (@ even (- n 1))))

main

(@odd 3)

Every program

A module statement like (module Simple in prelude out everything) has a name, and two signature expressions (prelude and everything). The 'in' expression tells programmer what things are defined in some other program file, and the 'out' expression tells what is defined by this file.

Signature expressions are formed by the syntax shown above. And every expression 'evaluates' to a set of definitions. In language E7 one may view this set by typing ':s sigExp' in the read-type-eval-print loop. For example see what definitions are in the prelude, see the transcript below.

enter Exp>
:s prelude

#nil::[a], #cons::(a-> [a]-> [a]), #pair::(a-> b-> (a . b))
(Bool ), (Int ), (Char ), (List a), (Pair a b)

Note that the file simple.e7 gets only these definition from the context (other files), since it has the in prelude clause in its module definition. We may subtract definitions from a set by using the hide expression.

enter Exp>
:s (hide prelude (Int Bool nil))

#cons::(a-> [a]-> [a]), #pair::(a-> b-> (a . b))
(Char ), (List a), (Pair a b)

We may create our own unique set of definitions by using an explicit signature.

enter Exp>
:s (sig (val x Int) (data (T) (#a Int) (#b)))
x::Int
#a::(Int-> T), #b::T
(T )

Or we may union together multiple sets by using the union operator.

enter Exp>
:s (union prelude (sig (val x Int) (data (T) (#a Int) (#b))))
x::Int
#a::(Int-> T), #b::T, #nil::[a], #cons::(a-> [a]-> [a]), #pair::(a-> b-> (a . b))
(T ), (Bool ), (Int ), (Char ), (List a), (Pair a b)

Finally, we may compute the set of everything defined in the file by using the everything sig expression.

enter Exp>
:s everything
odd::(Int-> Bool), even::(Int-> Bool), eq::(Int-> Int-> Bool), and::(Bool-> Bool-> Bool), ten::Int
#nil::[a], #cons::(a-> [a]-> [a]), #pair::(a-> b-> (a . b))
(Bool ), (Int ), (Char ), (List a), (Pair a b)

The 'in' and 'out' clauses compute a set of definitions for each file. The system checks that any imported file is only imported into a context where its 'in' set is available. The user may restrict what is exported from a file by using an 'out' clause that removes some definitions.

User defined types

To see how the user defines types, study the file types.e7


(module Types in prelude out everything)

(data (Tree a)
   (#tip a)
   (#fork (Tree a) (Tree a)))

(data (Color) (#red) (#blue) (#green))

(data (Result a) (#found a) (#notFound))

(global nil [a] (# nil))

(fun head h (x [h]) (!cons 0 x))
(fun tail [a] (x [a]) (!cons 1 x))
(fun fst a (x (a.b)) (!pair 0 x))
(fun snd b (x (a.b)) (!pair 1 x))
(fun null Bool (x [a]) (?nil x))
(fun consP Bool (x [a]) (?cons x))

{ Basic boolean support }
(fun and Bool (x Bool y Bool) (if x y x))
{ Equality on integers }
(fun eq Bool (x Int y Int) (@and (<= x y) (<= y x)))

(adt (Env a) [(Int . a)]
     (global empty (Env a) nil)

     (fun extend (Env a) (key Int object a table (Env a))
          (#cons (#pair key object) table))

     (fun lookup (Result a) (tab (Env a) key Int)
          (if (?nil tab) (#notFound)
              (if (@eq key (@fst (@head tab))) (#found (@snd (@head tab)))
                  (@lookup (@tail tab) key))))  )
main 0

We see that there are two ways to define a new type. A data definition (similar to Haskell) and an adt definition (an Abstract Data Type). A data definition introduces a new type with multiple constructors (tip, fork, red, blue, green, Found, and notfound). Such a type can be parameterized (like Tree and Result) or just be a type (like Color). Note that type names must start with a capital letter. There are three kinds of things one might want to do to a data type.

  1. Construct one. i.e. (#cons 4 (#nil))
  2. Access its sub components. i.e. (!cons 0 x)
  3. Test which constructor was used to construct it. i.e. (?cons x)
All of the familiar functions on (head,tail,fst,snd,null) can be defined using just these three kinds of operations.

An adt defines a type in terms of what operations it has. For example the type (Env a) has only the operations, empty, extend, and lookup. An adt has an implementation type. The implementation type for (Env a) is the type [(Char . a)]. Inside the adt the defined type (Env a) and the implementation type [(Char . a)] are the same. Outside the adt, the operations work only on values the abstract type. For example even though empty is defined to be nil. It can't be used that way.

enter Exp>
(#cons 4 empty)
user error (
*** Error, near "keyboard input
(#cons 4 empty)" (line 1, column 10)
[t92] =/= (Env t93) (Different types)
Checking construction arg empty
While inferring the type of
   empty
Expected type: [t92]
Computed type: (Env t93))

Consider an the E7 abstract data type (ADT) for environments, The ADT is defined in the file types.e7

(adt (Env a) [(Int . a)]
     (global empty (Env a) nil)

     (fun extend (Env a) (key Int object a table (Env a))
          (#cons (#pair key object) table))

     (fun lookup (Result a) (tab (Env a) key Int)
          (if (?nil tab) (#notFound)
              (if (@eq key (@fst (@head tab))) (#found (@snd (@head tab)))
                  (@lookup (@tail tab) key))))  )

Its signature is defined in the file envSig.e7.

What to do

  1. Your task is to write a new file that implements the EnvSig signature. Your file implementation should have the same functionality as the Env adt in file types.e7, but your file should use (unbalanced) binary search trees rather than lists as the implementation type. (Consult your favorite algorithms text for more details about binary search trees if you need them.)

    Your file should start with the prelude

    (module Env2 in prelude out (file "envSig.e7"))
    
    This means you cannot depend upon anything, except what is in the prelude, and you should export, only those things in the signature in file envSig.e7.

    Use the following definition of trees

    (data (Tree a)
       (#leaf)
       (#node Int a (Tree a) (Tree a)))
    

    Do not alter the existing Env interface and don't accidentally alter the behavior of the operators. In particular, remember that if an environment is extended twice with the same identifier, the more recent extension "hides" the previous one.

    Put your new module definition (only) into a file sol7A.e7 and submit it. (Note: you will almost certainly want to test your implementation, but don't include the testing code in your submission.)

  2. Repeat Exercise 1, but this time, instead of using binary trees, use the following implementation type for the abstract type: (Int -> (Result a)). Put your new module definition (only) into a file sol7B.e7 and submit it.