trickiness about the order of definitions in GUI code

53 views
Skip to first unread message

James Platt

unread,
May 6, 2020, 7:50:01 PM5/6/20
to Racket Users
I'm working on organizing and documenting some things and I have some code, below, which works but I don't understand why. Specifically, why don't I get an error about table3 not being defined?

This is a very simplified version of what I'm working on. What I actually want to do is put all the code related to creating a standard table editing menu in a file which I can then include wherever I want that menu. The full menu will be a lot of code. Unfortunately I can't seem to get this to work without either getting an error that row-edit-menu is not defined or that table3 is not defined. In other words, I want to be able to define table3 in one file which requires the file where row-edit-menu is defined but I can't seem to figure out how to get this to work. It works just fine when all the code is in one file, however.

James


#lang racket
(require racket/gui/base
qresults-list
)

;set up columns
(define (column1 data) (vector-ref data 0))
(define (column2 data) (vector-ref data 1))

(define my-columns
(list
(qcolumn "Column1" column1 column1)
(qcolumn "Column2"
(lambda (row)
;(displayln row)
;(displayln (db-row-ref row "Column2" headers 1))
(if (number? (column2 row)) (number->string (column2 row)) "");This allows a cell to be blank.
;(number->string (column2 row))
)
column2)

)
)

(define frame3 (new frame%
[label "myTable 3"]
[width 800]
[height 600]
))


(define row-edit-menu (new popup-menu% ))

(define info-menu-item (new menu-item%
(label "info")
(parent row-edit-menu)
(callback (lambda (menu-item event)
(message-box "Info"
(~a "You have selected " (length (send table3 get-selected-row-indexes)) " rows")
#f)
))
))

(define table3
(new (class qresults-list% (init) (super-new)
[parent frame3]
[pref-tag 'preferences-tag]
[selection-type 'multiple]
[right-click-menu row-edit-menu])
)

(send table3 setup-column-defs my-columns)

(send frame3 show #t)

(send table3 add-row (vector "R1C1" 10))
(send table3 add-row (vector "R2C1" 11))
(send table3 add-row (vector "R3C1" 12))
(send table3 add-row (vector "R4C1" 13))


Jon Zeppieri

unread,
May 6, 2020, 8:49:54 PM5/6/20
to James Platt, Racket Users
On Wed, May 6, 2020 at 7:50 PM James Platt <j...@biomantica.com> wrote:
>
> I'm working on organizing and documenting some things and I have some code, below, which works but I don't understand why. Specifically, why don't I get an error about table3 not being defined?

The reason you don't get an error for the code you posted is because
the mention of `table3` (before its definition) in `info-menu-item` is
inside a function body. The referent of the name `table3` isn't
required to define `info-menu-item`; it's sufficient to know that it
refers to _something_, and that it will be available when that
function is (later) called.

> In other words, I want to be able to define table3 in one file which requires the file where row-edit-menu is defined but I can't seem to figure out how to get this to work.

It's a bit trickier to define these things in separate files, because
their definitions refer to each other (though indirectly in this
case), and the module system does not tolerate cyclic dependencies.
The most straightforward way to break the cycle would be to take
advantage of the fact that `table3` and `info-menu-item` each depends
on `row-edit-menu`, and `info-menu-item` further depends on `table3`,
but `row-edit-menu` doesn't depend on either. So the dependencies
aren't really cyclical. You can divide stuff up into separate modules
like so:

menu.rkt
```
#lang racket

(require racket/gui/base)

(provide frame3
row-edit-menu)

(define frame3
(new frame%
[label "myTable 3"]
[width 800]
[height 600]))

(define row-edit-menu (new popup-menu%))
```


table.rkt
```
#lang racket

(require racket/gui/base
qresults-list
"menu.rkt")

(provide table3
add-rows)

;set up columns
(define (column1 data) (vector-ref data 0))
(define (column2 data) (vector-ref data 1))

(define my-columns
(list
(qcolumn "Column1" column1 column1)
(qcolumn "Column2"
(λ (row)
;(displayln row)
;(displayln (db-row-ref row "Column2" headers 1))
(if (number? (column2 row)) (number->string (column2
row)) "");This allows a cell to be blank.
;(number->string (column2 row))
)
column2)))

(define table3
(new (class qresults-list% (init) (super-new))
[parent frame3]
[pref-tag 'preferences-tag]
[selection-type 'multiple]
[right-click-menu row-edit-menu]))

(define (add-rows)
(send table3 add-row (vector "R1C1" 10))
(send table3 add-row (vector "R2C1" 11))
(send table3 add-row (vector "R3C1" 12))
(send table3 add-row (vector "R4C1" 13)))
```


menu-item.rkt
```
#lang racket

(require racket/gui/base
"menu.rkt"
"table.rkt")

(provide info-menu-item)

(define info-menu-item
(new menu-item%
[label "info"]
[parent row-edit-menu]
[callback (λ (menu-item event)
(message-box "Info"
(~a "You have selected " (length (send
table3 get-selected-row-indexes)) " rows")
#f))]))
```


main.rkt
```
#lang racket

(require "menu.rkt"
"table.rkt")

(send frame3 show #t)
(add-rows)
```

Hope that helps.

- Jon

Alex Harsanyi

unread,
May 6, 2020, 9:27:30 PM5/6/20
to Racket Users
Every problem can be solved by adding another level of indirection (except perhaps having too many  levels of indirection :-) ).  You can put the code below in a separate file:

(define (make-info-menu-item parent target-table)

 
(new menu-item%
     
(label "info")

     
(parent parent)

     
(callback (lambda (menu-item event)
                 
(message-box "Info"

                   
(~a "You have selected " (length (send target-table get-selected-row-indexes)) " rows")
                   
#f)))))

And use it like so:

(require "info-menu-item.rkt")

(define row-edit-menu (new popup-menu% ))
(define table3 ...)
(define info-menu (make-info-menu-item row-edit-menu table3)

You could also have just one "make-row-edit-menu" function which creates all menu items.

Alex.

James Platt

unread,
May 6, 2020, 10:20:47 PM5/6/20
to Racket Users
Thanks, Jon. I'm going to try Alex's solution first but I may get back to this.
> --
> You received this message because you are subscribed to the Google Groups "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CAKfDxxwofLFVTkcGii%2BtLGKYCBFVNY2Tw9m8ZNjR--Hh_FRtEw%40mail.gmail.com.

James Platt

unread,
May 6, 2020, 10:48:19 PM5/6/20
to Racket Users
Thanks, Alex. I actually tried something very similar to this earlier but failed. What I should have done is to try it with my simplified code and then I would have realized it was an approach that could work. The complication is that there is much more stuff going on in my production code. For example, the popup menu is defined with a demand-callback function to enable and disable menu items, so that something which operates on a selection will be grayed out if there is no selection. This also depends on the table definition. I'll try and figure out how to make that work but, like you said, it's just a matter of more indirection.

James
> --
> You received this message because you are subscribed to the Google Groups "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/01e04764-42b2-4619-8aff-32fbf4d68269%40googlegroups.com.

Philip McGrath

unread,
May 7, 2020, 12:45:01 AM5/7/20
to Jon Zeppieri, James Platt, Racket Users
On Wed, May 6, 2020 at 8:49 PM Jon Zeppieri <zepp...@gmail.com> wrote:
It's a bit trickier to define these things in separate files, because
their definitions refer to each other (though indirectly in this
case), and the module system does not tolerate cyclic dependencies.
The most straightforward way to break the cycle would be to take
advantage of the fact that `table3` and `info-menu-item` each depends
on `row-edit-menu`, and `info-menu-item` further depends on `table3`,
but `row-edit-menu` doesn't depend on either. So the dependencies
aren't really cyclical.

When possible, finding ways to avoid cyclic dependencies in the first place is definitely advisable: even if you can get Racket to understand them, mere human beings may find them confusing.

But sometimes there really are entanglements between units of code that you want to logically separate: then, as Alex said, you need an indirection. For a relatively simple case, a simple solution using familiar constructs (like `lambda`) is the right choice, but complicated cycles seem to turn up especially often in complicated, large-scale code.

Rather than designing an ad hoc system of indirection that can handle all of the complexity,* I suggest using the one that already exists: units, Racket's original, first-class (rather than first-order) module system, offer support for cyclic dependencies. In fact, they are used in the implementation of Racket's GUI framework to address precisely this problem.

For this example it ends up being a bit contrived (I would probably use `class` to define a `row-edit-menu%` that takes a table as an initialization argument), but here's a way of writing it in a single file using units: I've posted a Gist showing the division into multiple files, which is straight-forward.

#lang racket/gui

(require qresults-list)

;; A signatures describes an interface.

(define-signature row-edit-menu^
  (row-edit-menu))

(define-signature table3^
  (table3))

(define-signature frame3^ extends table3^
  (frame3))

;; A unit that exports a signature must
;; implement the definitions it specifies.

;; Units can also import signatures,
;; which allows the body of the unit to
;; refer to definitions from the imported signatures.
;; Before the unit is invoked,
;; it must be linked together with other units
;; that that export the signatures it imports,
;; which will suply the actual implementations.

;; Code in the body of the unit is run when the unit is invoked.

(define-unit row-edit-menu@
  (import table3^)
  (export row-edit-menu^)
 
  (define row-edit-menu
    (new popup-menu%))

 
  (define info-menu-item
    (new menu-item%
         [label "info"]
         [parent row-edit-menu]
         [callback
          (λ (menu-item event)
            (define num-selected
              (length (send table3 get-selected-row-indexes)))
            (message-box "Info"
                         (~a "You have selected " num-selected " rows")
                         #f))])))


(define-unit table3@
  (import row-edit-menu^)
  (export frame3^)

  (init-depend row-edit-menu^)

 
  (define frame3
    (new frame%
         [label "myTable 3"]
         [width 800]
         [height 600]))

  (define table3
    (new (class qresults-list%

           (super-new))
         [parent frame3]
         [pref-tag 'preferences-tag]
         [selection-type 'multiple]
         [right-click-menu row-edit-menu])))


;; Invoke the units and introduce the definitions they
;; export into this scope:

(define-values/invoke-unit/infer
  (link row-edit-menu@
        table3@))

;; Run the demo:

(send table3
      setup-column-defs
      (let ([column1
             (λ (data) (vector-ref data 0))]
            [column2
             (λ (data) (vector-ref data 1))])

        (list (qcolumn "Column1" column1 column1)
              (qcolumn "Column2"
                       (λ (row)
                         ;; allows a cell to be blank

                         (if (number? (column2 row))
                             (number->string (column2 row))
                             ""))
                       column2))))


(send table3 add-row (vector "R1C1" 10))
(send table3 add-row (vector "R2C1" 11))
(send table3 add-row (vector "R3C1" 12))
(send table3 add-row (vector "R4C1" 13))

(send frame3 show #t)

* Even when specific requirements eventually led me to implement my own system, as I discussed a bit in my RacketCon talk, it helped to have used units and looked at a bit of their implementation. I still do use units in other parts of the Digital Ricoeur codebase.

-Philip

Dexter Lagan

unread,
May 7, 2020, 5:10:11 AM5/7/20
to James Platt, Racket Users
Hi James,

  Like others said, Unit is the proper solution, but to add my 2c, I was able to avoid this problem altogether by using these two simple tricks :
1) add the controls in the order of their requirement (defining table3 before info-menu-item), then re-ordering the controls before displaying the window, as follows:

(send frame3 change-children
      (λ (children)
        (list ...
              info-menu-item
              table3
              ...)))

2) to get around the problem of separating gui and logic code, one solution is to create an englobing 'display-gui' function which takes the callback func names as parameters. That allows one to move all the callback functions outside of the GUI, and pipe them through from another module. For example:

main.rkt:

;;; defs

(define (menu3-callback-proc param1 ...)
   ...)

;;; main

(show-gui  menu3-callback-proc ...)

gui.rkt:
(define (show-gui menu3-callback-proc-name ...)

  gui code...

  )

Let me know if this helps...

Dex

--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.

James Platt

unread,
May 7, 2020, 12:32:02 PM5/7/20
to Racket Users
On May 7, 2020, at 12:44 AM, Philip McGrath wrote:

> Rather than designing an ad hoc system of indirection that can handle all of the complexity,* I suggest using the one that already exists: units, Racket's original, first-class (rather than first-order) module system, offer support for cyclic dependencies. In fact, they are used in the implementation of Racket's GUI framework to address precisely this problem.

Thanks. I need to read up on Units. Like I said, my production code is much more complicated than the example I posted so I'm sure that better organization will really help in the long run.

James
Reply all
Reply to author
Forward
0 new messages