2009-02-24

Call for Participation: Writing Typed Scheme wrapper modules

Typed Scheme is a language for writing PLT Scheme programs that are statically checked for type errors. Right now, Typed Scheme handles a large portion of the PLT Scheme language. However, to use Typed Scheme effectively, we need libraries that work with it. For this task, we're looking for help.

We'd like volunteers for writing wrapper modules that adapt untyped libraries to Typed Scheme. This task is very easy for the smallest libraries, but can be much more complicated for larger, complex libraries.

There's a preliminary guide for conversion here and a list of modules to adapt, and their current status is available here.

Questions about this project, and about developing with Typed Scheme in general, can be asked on plt-dev@list.cs.brown.edu , and questions or comments can be sent directly to me.

We hope to continue making Typed Scheme into a useful tool for PLT Scheme programmers.

Thanks,
Sam, Stevie, and Matthias

2009-02-18

Steering Scheme

Election time is here again. A couple more days and the Scheme community will have a set of new steer-ers.

What should we want from a steering committee?

I have argued at this place before that good language design needs a feedback loop. Language designers write down specs; language implementers translate those specs into compilers and interpreters; programmers use these implementations to produce useful software. The loop comes in when implementers inform designers of flaws, inconsistencies, mistakes, errors, and other internal consistency problems in the specs. This is clearly happening with R6RS, and it is good. Implementers are a biased bunch, however. After all, they work on just one kind of program, and in a highly specialized domain that has been mined for a long time. How can you trust them? [*]

The loop becomes truly useful when people write large software systems (not just compilers, because they really are special cases!) and find that the language fails them in some way. Such failures can come in a number of flavors. For a document such as R6RS, we should hope that programmers can discover problems with porting systems that are apparently due to ambiguities, flaws, mistakes, roaches in the actual document (as opposed to a specific implementation).

So what does this have to do with the steering committee?

The last thing we want from a steering committee is a radical commitment to change (whatever it may be); a prejudice concerning R6RS; a closed mind about the size of "Scheme" (it's too large, it's too small); a willingness to steer without making observations. A steering committee of overbearing curmudgeons is not what we want.

What we do want is a committee that is willing to figure out how the listening is going to happen; how we can possibly finance a systematic way of listening (writing NSF grants, anyone?); how the feedback is best channeled into language design.

Let's hope we get such a steering committee. The Scheme community deserves it.


[*] No I am not naive enough to think that language implementers don't design languages, and that implementers don't program systems other than implementations. I am just skeptical that it is easy for them to separate out their various roles in an objective manner, even if many of them are able to think at several different levels about programs.

2009-02-14

New Contract-Related Features

In SVN I've added three new major features that involve contracts. One allows for more fine-grained control of contracts, and the other two allow for the use of contracts with signatures and units.

Contract Regions

Contract regions allow the programmer to protect a region of code with a contract boundary. In addition to the wrapped code, the programmer also provides a name for the region which is used in blame situations and a list of exported variables which can either be protected with contracts or unprotected. The region provides a true contract boundary, in that uses of contracted exports within the region are unprotected. Contract regions are specified with the with-contract form. The following contract region defines two mutually recursive functions:

(with-contract region1
 ([f (-> number? boolean?)]
  [g (-> number? boolean?)])
 (define (f n) (if (zero? n) #f (g (sub1 n))))
 (define (g n) (if (zero? n) #t (f (sub1 n)))))

The internal calls to f and g are uncontracted, but calls to fand g outside this region would be appropriately contracted. First-order checks are performed at the region, so the following region:

(with-contract region2
 ([n number?])
 (define n #t))

results in the following error:

(region region2) broke the contract number? on n; expected <number?>, given: #t

Notice that the blame not only gives the name of the region, but describes what type of contract boundary was involved.

For contracting a single definition, there is the define/contract form which has a similar syntax to define, except that it takes a contract before the body of the definition. To compare the two forms, the following two definitions are equivalent:

(with-contract fact
 ([fact (-> number? number?)])
 (define (fact n)
   (if (zero? n) 1 (* n (fact (sub1 n))))))

(define/contract (fact n)
 (-> number? number?)
 (if (zero? n) 1 (* n (fact (sub1 n)))))

First order checks are similarly performed at the definition for define/contract, so

(define/contract (fact n)
 (-> number?)
 (if (zero? n) 1 (* n (fact (sub1 n)))))

results in

(function fact) broke the contract (-> number?) on fact; expected a procedure that accepts no arguments without any keywords, given: #<procedure:fact>

Signature Contracts

In addition to contract regions, units are also now contract boundaries. One way to use contracts with units is to add contracts to unit signatures via the contracted signature form.

(define-signature toy-factory^
 ((contracted
   [build-toys (-> integer? (listof toy?))]
   [repaint    (-> toy? symbol? toy?)]
   [toy?       (-> any/c boolean?)]
   [toy-color  (-> toy? symbol?)])))

Notice that contracts in a signature can use variables listed in the signature.

Now if we take the following implementation of that signature:

(define-unit simple-factory@
 (import)
 (export toy-factory^)
  
 (define-struct toy (color) #:transparent)
  
 (define (build-toys n)
   (for/list ([i (in-range n)])
     (make-toy 'blue)))
  
 (define (repaint t col)
   (make-toy col)))

We get the appropriate contract checks on those exports:

> (define-values/invoke-unit/infer simple-factory@)
> (build-toys 3)
(#(struct:toy blue) #(struct:toy blue) #(struct:toy blue))
> (build-toys #f)
top-level broke the contract (-> integer? (listof toy?))
 on build-toys; expected , given: #f

As before, uses of contracted exports inside the unit are not checked.

Since units are contract boundaries, they can be blamed appropriately. Take the following definitions:

(define-unit factory-user@
 (import toy-factory^)
 (export)
 (let ([toys (build-toys 3)])
   (repaint 3 'blue)))

(define-compound-unit/infer factory+user@
 (import) (export)
 (link simple-factory@ factory-user@))

When we invoke the combined unit:

> (define-values/invoke-unit/infer factory+user@)
(unit factory-user@) broke the contract
 (-> toy? symbol? toy?)
on repaint; expected , given: 3

Unit Contracts

However, we may not always be able to add contracts to signatures. For example, there are many already-existing signatures in PLT Scheme that one may want to implement, or a programmer may want to take a unit value and add contracts to it after the fact.

To do this, there is the unit/c contract combinator. It takes a list of imports and exports, where each signature is paired with a list of variables and their contracts for each signature. So if we had the uncontracted version of the toy-factory^ signature:

(define-signature toy-factory^
 (build-toys repaint toy? toy-color))

the following contracts would be appropriate for a unit that imports nothing and exports that signature:

(unit/c (import) (export))
(unit/c (import) (export toy-factory^))
(unit/c
 (import)
 (export (toy-factory^
          [toy-color (-> toy? symbol?)])))
(unit/c
 (import)
 (export (toy-factory^
          [build-toys (-> integer? (listof toy?))]
          [repaint    (-> toy? symbol? toy?)]
          [toy?       (-> any/c boolean?)]
          [toy-color  (-> toy? symbol?)])))

Unit contracts can contain a superset of the import signatures and a subset of the export signatures for a given unit value. Also, variables that are not listed for a given signature are left alone when the contracts are being added.

Since the results of applying unit/c is a new unit, then adding a contract can cause link inference to fail. For example, if we change the definition of simple-factory@ above to

(define/contract simple-factory@
 (unit/c
  (import)
  (export (toy-factory^
           [build-toys (-> integer? (listof toy?))]
           [repaint    (-> toy? symbol? toy?)]
           [toy?       (-> any/c boolean?)]
           [toy-color  (-> toy? symbol?)])))
 (unit
   (import)
   (export toy-factory^)
  
   (define-struct toy (color) #:transparent)
  
   (define (build-toys n)
     (for/list ([i (in-range n)])
       (make-toy 'blue)))
  
   (define (repaint t col)
     (make-toy col))))

Then when we try to combine it with the factory-user@ unit, we get:

define-compound-unit/infer: not a unit definition in: simple-factory@

One way to solve this is to use define-unit-binding to set up the static information for the new contracted value. Another possibility for unit definitions is to use define-unit/contract:

(define-unit/contract simple-factory@
 (import)
 (export (toy-factory^
          [build-toys (-> integer? (listof toy?))]
          [repaint    (-> toy? symbol? toy?)]
          [toy?       (-> any/c boolean?)]
          [toy-color  (-> toy? symbol?)]))

 (define-struct toy (color) #:transparent)

 (define (build-toys n)
   (for/list ([i (in-range n)])
     (make-toy 'blue)))

 (define (repaint t col)
   (make-toy col)))

More about these features can be found in the Reference, and a short section about signature and unit contracts has been added to the Guide.