Klunky structure code with generics

I am having trouble figuring out how to represent the dta structures I want in a convenient way.

Here's a very stripped-down example, with comments indicating what I'm finding klunky.
(I found classes confusing -- should I be using them instead anyway?)

#lang racket/gui

; example for request for assistance

(require racket/generic)

(define-generics anything
  (gdescription anything)
  (gdescribe anything)
  (gget-location anything)
  (gset-location! anything l)
  )

; Klunky that I need to define these, which are essentially renamings.
(define (description x) (gdescription x))
(define (describe x) (gdescribe x))
(define (get-location x) (gget-location x))
(define (set-location! x l) (gset-location! x l))

(struct location (name)
  #:methods gen:anything
  [
   (define (gdescription loc) (format "~s" (location-name loc) ))
   (define (gdescribe loc) (printf (description loc)))
   ]
  )

(struct player (name [location #:auto #:mutable])
  #:auto-value '()
  #:methods gen:anything
  [
   (define (gdescription player)
     (format "~s are in ~s\n"
             (player-name player)
             ; Here's where I need description instead of gdescription.
             ; If I just call gdescription I end up with a recursive call to
             ; this function instead of the generic one, and
             ; it complains that [player-location player] isnt a player.
             ; Of course it isn't.  It's a location.
             (description [player-location player])
             ; And that's the only reason I'm defining gdescription
             ; as well as description.
             )
     
     )
   (define (gdescribe player)
     (printf (description player) )
     )
   ; Do I really have to define set-location separately
   ; for each structure that uses gen:anything?
   ; Is there anything like extending an old structure when defining a new one?
   ; or mixins?
   (define (gset-location! player x) (set-player-location! player x))
   ]
)

(define (place o l)
  (set-location! o l)
  )

(provide player describe  location place)

and test it with mstr-test.rkt

#lang racket/gui
(require "str.rkt")

(define living-room (location "living room")) 
(define front-hall (location "front hall"))
(describe living-room)

(define p (player "You"))
(place p living-room)

(describe p)

-- hendrik

The way this is intended to be done is that you use define/generic to avoid the recursion (see the example below the documentation of define/generic).

But you can also create a macro on top of that to make this easier for you to write:

#lang racket

(require syntax/parse/define
         racket/generic)

(define-syntax-parse-rule (define/genmeth (name:id . args)
                            body ...+)
  (begin
    (define/generic super name)
    (define (name . args)
      (let-syntax ([name (make-rename-transformer #'super)])
        body ...))))

(define-generics anything
  (description anything)
  (describe anything)
  (get-location anything)
  (set-location! anything l))

(struct location (name)
  #:methods gen:anything
  [(define/genmeth (description loc)
     (format "~s" (location-name loc) ))
   (define/genmeth (describe loc)
     (printf (description loc)))])

(struct player (name [location #:auto #:mutable])
  #:auto-value '()
  #:methods gen:anything
  [(define/genmeth (description player)
     (format "~s are in ~s\n"
             (player-name player)
             (description [player-location player])))
   (define/genmeth (describe player)
     (printf (description player)))
   (define/genmeth (set-location! player x)
     (set-player-location! player x))])

(define (place o l)
  (set-location! o l))

(provide player describe location place)

1 Like

Thank you. That works elegantly.
But it will take me a while to fully understand make-rename-transformer.
define/generic is now clear to me.

-- hendrik

One thing that I just noticed is that define/genmeth only works for the method that you are defining. If, for example, you changed player‘s describe to:

(define/genmeth (describe player)
     (display (description (player-location player))))

you will get an error, because description is bound to the immediate method that you define, which only handles player, not the generic description which can handle a location.

We could adjust the macro to fix this issue by making all calls generic.

#lang racket

(require syntax/parse/define
         racket/generic)

(define-syntax-parse-rule (define-meths [(name:id . args)
                                         body ...+] ...)
  #:with (super ...) (generate-temporaries (attribute name))
  (begin
    (define/generic super name) ...
    (define (name . args)
      (let-syntax ([name (make-rename-transformer #'super)] ...)
        body ...))
    ...))

(define-generics anything
  (description anything)
  (describe anything)
  (get-location anything)
  (set-location! anything l))

(struct location (name)
  #:methods gen:anything
  [(define-meths
     [(description loc) (format "~s" (location-name loc) )]
     [(describe loc) (printf (description loc))])])

(struct player (name [location #:auto #:mutable])
  #:auto-value '()
  #:methods gen:anything
  [(define-meths
     [(description player)
      (format "~s are in ~s\n"
              (player-name player)
              (description [player-location player]))]
     [(describe player) (printf (description player))]
     [(set-location! player x) (set-player-location! player x)])])

(define (place o l)
  (set-location! o l))

(provide player describe location place)

My understanding is that generic calls are more expensive than direct calls, so it might not be a good idea to use define-meths when define/genmeth suffices.

I switched to classes from generics because I am migrating my code base to Typed Racket, and I found that classes work quite nicely for representing the notion of an abstract interface, or an abstract base class.

Now, as somebody told me, using classes for setting up an abstract interface is probably an overkill, but after playing around with some other options in Typed Racket I found that classes were the least clunky one for me.

2 Likes