2
votes

For a project I am creating a class driver% which is supposed to be a layer of abstraction over different modules providing the same procedures. The class would be initialized with an argument that specifies the module to be used.

Additionally, I would like driver% to expose the same procedures but without side effects, to facilitate unit testing classes that use the driver.

Consider the following:

module_one.rkt
#lang racket
(provide foo)
(define (foo)
  (display "called from "module one"))
module_two.rkt
#lang racket
(provide foo)
(define (foo)
  (display "called from "module two"))
driver.rkt
#lang racket
(require (prefix-in one: "module_one.rkt")
         (prefix-in two: "module_two.rkt"))

(provide driver%)

(define driver%
  (class object%
         (super-new)
         (init driver-choice)
         (define choice driver-choice)
         (define/public (foo)
           (case choice
             [(1) (one:foo)]
             [(2) (two:foo)]
             [else void]))))       

This fulfills the above requirements, but is not very elegant: for each exposed procedure, another case expression will have to be added. This seems unnecessary, as the choice for an API is made when the class is instantiated, so the choice will be the same everywhere.

What would be a more acceptable solution to this problem? I have looked into using local-require, but this does not seem to work with define/public.

Many thanks!

1

1 Answers

4
votes

You can use dynamic-require to require a module dynamically. You can also define a macro to reduce repetitive code.

But first of all, note that it's possible to avoid class altogether:

;; module-one.rkt
#lang racket
(provide foo bar)
(define (foo) (displayln "called foo from module-one"))
(define (bar) (displayln "called bar from module-one"))
;; module-two.rkt
#lang racket
(provide foo bar)
(define (foo) (displayln "called foo from module-two"))
(define (bar) (displayln "called bar from module-two"))
;; driver.rkt
#lang racket

(define ((make-driver choice) method-name)
  (case choice
    [(1) ((dynamic-require "module-one.rkt" method-name))]
    [(2) ((dynamic-require "module-two.rkt" method-name))]
    [else (void)]))

(define a-driver (make-driver 1))
(a-driver 'foo)
(a-driver 'bar)

(define b-driver (make-driver 2))
(b-driver 'foo)
(b-driver 'bar)

outputs:

called foo from module-one
called bar from module-one
called foo from module-two
called bar from module-two

In case you really want to use class, here's one possibility:

;; driver.rkt
#lang racket

(require syntax/parse/define)

(define-simple-macro (driver 
                       #:modules ([mod-id mod-path] ...)
                       #:methods (methods ...))
  (class object% (super-new)
    (init-field driver-choice)
    (begin
      (define/public (methods)
        (define method-name 'methods)
        (case driver-choice
          [(mod-id) ((dynamic-require mod-path method-name))]
          ...
          [else (void)])) ...)))

(define driver%
  (driver
   #:modules ([1 "module-one.rkt"] [2 "module-two.rkt"])
   #:methods (foo bar)))

(define a-driver (new driver% [driver-choice 1]))
(send a-driver foo)
(send a-driver bar)

(define b-driver (new driver% [driver-choice 2]))
(send b-driver foo)
(send b-driver bar)

outputs:

called foo from module-one
called bar from module-one
called foo from module-two
called bar from module-two