Home > Programming > Scheming in Units

Scheming in Units

March 31st, 2009

After working with modules in a language like Standard ML, modular abstraction in other programming languages can feel somewhat primitive. One sometimes wishes to abstract an inter-module dependency, or, put another way, parameterize a module by the implementation of an interface. In object-oriented terms, this is a functional analogy of programming to an interface. To this end, PLT Scheme provides a mechanism called Units. Unfortunately, I found working through the Guide and Reference (standard documentation) rather hard going as usage is somewhat complicated by the availability of syntax-directed static analysis in some situations but not others. What I really missed in the documentation was a simple, generic example of working with different implementations of one signature. What follows is an example program that demonstrates several variations of just that technique with PLT Scheme 4.1.5. This is by no means a replacement for the Guide or Reference, but instead a supplementary example. One additional note: I am deliberately using several variations of the syntax for working with Units in order to provide examples of a few different styles.

The program we are writing is one that asks the user for his or her name, and then displays a greeting. However, the interface logic doesn’t specify the implementation for either of those actions. In this toy example, the idea is to provide implementations in different human languages. Note that the entire implementation is abstract, not just the string data. So, for example, the action of asking for a name could involve very different dialogues in different implementations (e.g. one question, or a series of questions warming up to asking for a name), or even a network transport layer for interacting with a remote user. The key feature is that the interface logic is entirely parameterized by the two actions I am calling ask-for-name and greet.

Let’s make that parameterization concrete with the file service-sig.ss

;;; The service we are abstracting over is one that provides functions for 
;;; basic user interaction.
#lang scheme

(provide service^)

(define-signature service^
  (ask-for-name
   greet))

With that signature defined, we can define our user interaction logic. Here is the file interaction.ss.

;;; The interaction logic is dependent on a service for directly interfacing
;;; with the user. In this example, the functions ask-for-name and greet will
;;; be supplied by some unit implementing the service^ signature.
#lang scheme

(require "service-sig.ss")

(provide user-interface^ interaction@)

(define-signature user-interface^
  (prompt-user))
  
(define-unit interaction@
  (import service^)
  (export user-interface^)
  (define (prompt-user)
    (let ((name (ask-for-name)))
      (greet name))))

We’re going to need an implementation of our “service,” so let’s start with an English language version, service-impl1.ss

;;; An English language implementation of the service^ signature.
#lang scheme/unit

(require "service-sig.ss")

(import)
(export service^)

(define (ask-for-name) (printf "What is your name? ") (read-line))
(define (greet name) (printf "Well, hello there, ~a!~n" name))

That’s a lot of foundational work; let’s see how it comes together before going any further. The first version of our program, service-program1.ss

;;; Basic usage of the interaction unit. We supply interaction@ with an 
;;; implemenation of the service^ signature so that we can use it.
#lang scheme

(require "service-impl1.ss")
(require "service-impl2.ss")
(require "interaction.ss")

;; If we don't want programatic linking, then we can really lean on inference.
;; Change service-impl1@ to service-impl2@ for a different language.
(define-compound-unit/infer my-program
  (import)
  (export user-interface^)
  (link service-impl1@ interaction@)) 

;; Brings an implementation of the user-interface^ signature into scope.
(define-values/invoke-unit/infer my-program)

;; Kick off the interactive program
(prompt-user)

I’ve snuck in another implementation of our service^ signature. Here is the Spanish language version, service-impl2.ss,

;;; An Spanish language implementation of the service^ signature.
#lang scheme/unit

(require "service-sig.ss")

(import)
(export service^)

(define (ask-for-name) (printf "¿Cómo te llamas? ") (read-line))
(define (greet name) (printf "¡Bueno! ¿Cómo estás, ~a?~n" name))

One can play around with service-program1.ss as indicated in the comments. We have now achieved our goal of writing our program in such a way that we do not need to touch the interaction logic in order to supply different implementations of our service^ signature.

But Units can be taken further! Units are first class in PLT Scheme, so we can work with them at runtime. The requirement that we modify the source of service-program1.ss to change language front-ends feels a bit clumsy, so let’s write a program that will select which implementation to use for itself, service-program2.ss

;;; Units are first class in PLT Scheme, so we can programatically control 
;;; how they are linked together. In this variation, we bind an identifier 
;;; to a particular Unit depending on the user's choice of language. We 
;;; then link the interaction Unit with that new binding in order to 
;;; execute our program.
#lang scheme

(require "service-impl1.ss")
(require "service-impl2.ss")
(require "interaction.ss")
(require "service-sig.ss")

(printf "Choose your language: (1) English or (2) for Spanish")
(define language (if (regexp-match #px"2" (read-line))
                     'spanish
                     'english))

;; If we are willing to bind a temporary at the top-level, then we can 
;; use a define-unit form which will let us take advantage of /infer 
;; forms when using the unit. 
(define-unit-binding impl@ 
  (cond 
    ((eq? language 'english) service-impl1@)
    ((eq? language 'spanish) service-impl2@))
  (import)
  (export service^))

(define-compound-unit/infer my-program
  (import)
  (export user-interface^)
  (link impl@ interaction@))

;; Brings an implementation of the user-interface^ signature into scope.
(define-values/invoke-unit/infer my-program)

;; Kick off the interactive program
(prompt-user)

For one final variation, the top-level binding of impl@ bothers me a bit since it is purely a detail of my-program‘s implementation. In service-program3.ss, we move impl@ into my-program‘s definition.

;;; We can achieve programatic linking without binding a new identifier at the 
;;; top-level, but this strategy reduces the degree to which the Unit 
;;; system is able to infer imports and exports.
#lang scheme

(require "service-impl1.ss")
(require "service-impl2.ss")
(require "interaction.ss")
(require "service-sig.ss")

(printf "Choose your language: (1) English or (2) for Spanish")
(define language (if (regexp-match #px"2" (read-line))
                     'spanish
                     'english))

;; An example of using first class units. We have to make imports
;; and exports much more explicit since we are not using a top-level 
;; form for binding impl@, our temporary unit.
(define my-program
  (let ((impl@ (cond
                 ((eq? language 'english) service-impl1@)
                 ((eq? language 'spanish) service-impl2@))))
    (compound-unit
     (import)
     (export UI)
     (link (((UI : user-interface^)) interaction@ Lang)
           (((Lang : service^)) impl@)))))

;; Brings an implementation of the user-interface^ signature into scope.
(define-values/invoke-unit my-program (import) (export user-interface^))

;; Kick off the interactive program
(prompt-user)

All the source files may be found in this archive.

Share
Categories: Programming Tags: , ,
  1. No comments yet.
Comments are closed.