CS558 Homework #7
Due 5:00 PM, Monday, March 2, 2015

Homework must be submitted via D2L. All submitted files (2 for this assignment sol7A.hs and sol7B.e7) 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. For example.

-- Homework 7  Tom Smith   tom_smith@gmail.com

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.

Language E7 is different from our other language in that it is composed of two kinds of files: Program files (i.e. xxx.e7) and Signature files (i.e. xxx.sig). Signature files contain information that describes a set of names and their associated attributes. Attributes include things like arities and types. E7's concretized abstract syntax is given by the following grammar:

sigFile := { signature } '(' defsig Id sigExp ')'
progFile := { signature } module { globaldef | fundef | datadef | adtdef | import }  'main' exp

signature := '(' 'signature' Id string ')'          -- "Id" names a set of items, "string" tells which file.
module := '(' 'module' Id in sigExp out sigExp ')'

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 like 'a'
| string                                    -- familiar "abc" syntax for list of Char
| '(' = exp exp ')                          -- Character equality (ill-typed on other things)

| '(' '@' exp { exp } ')'                 -- function application
| '(' 'lambda' ( {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 }  ')'

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

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

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))))


(@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 to some set of definitions are available in simple.e7, we load the file and then use the ':s' feature. See the transcript below.
enter Exp>
:s prelude

*** Types
(Bool ), (Int ), (Char ), (List a), (Pair a b)
*** Vars

*** Constructors
#cons::(a-> [a]-> [a])
#pair::(a-> b-> (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)

*** Types
(Char ), (List a), (Pair a b)
*** Vars

*** Constructors
#cons::(a-> [a]-> [a])
#pair::(a-> b-> (a . b))
enter Exp>

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

:s (sig (val x Int) (data (T) (#a Int) (#b)))

*** Types
*** Vars
*** Constructors
#a::(Int-> 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))))

*** Types
(T ), (Bool ), (Int ), (Char ), (List a), (Pair a b)
*** Vars
*** Constructors
#a::(Int-> T)
#cons::(a-> [a]-> [a])
#pair::(a-> b-> (a . b))

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

:s everything

*** Types
(Bool ), (Int ), (Char ), (List a), (Pair a b)
*** Vars
odd::(Int-> Bool)
even::(Int-> Bool)
eq::(Int-> Int-> Bool)
and::(Bool-> Bool-> Bool)
*** Constructors
#cons::(a-> [a]-> [a])
#pair::(a-> b-> (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 describe exactly the set of items exported from a file by using an 'out' clause. The system checks that there is a defintion for every item in the 'out' set. Definitions not in the 'out' set are "hidden" and are not exported.

Signature Declarations and Files

To be useful, a language with a module system must be able to talk about a set of items independently of the item's definition. This is the role of signature files. Consider the signature file C.sig.
(signature A "A.sig")
(signature B "B.sig")

(defsig C (union A B (sig (val c Int) (val d Bool))))

A signature file denotes a set of items, and binds this set to a signature variable. In the file above, the signature variable is C. Every signature file

We use signature declarations (like (signature A "A.sig")) at the top of both signature files and program files. The variables they bind (like A) live in a separate name space from program variables. These variables always start with a upper case letter, and can be used in sigExp's. A more usefule signature file for table lookup is given below.

(defsig EnvSig
  (sig (type (Env a))
       (data (Result a) (#found a) (#notFound))
       (val lookup ((Env a) -> Int -> (Result a)))
       (val extend (Int -> a -> (Env a) -> (Env a)))
       (val empty (Env a))))

User defined types

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

(signature EnvSig "env.sig")

(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))))  )


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
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 env.sig.

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 have the following module declaration.

    (module Env2 in prelude out (file "env.sig"))
    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 env.sig.

    Use the following definition of trees

    (data (Tree a)
       (#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.