Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean way to implement a list of checkboxes? #42

Open
cloudrac3r opened this issue May 29, 2023 · 2 comments
Open

Clean way to implement a list of checkboxes? #42

cloudrac3r opened this issue May 29, 2023 · 2 comments

Comments

@cloudrac3r
Copy link
Contributor

Hiya! This is an open-ended issue without a strict definition of completed.

I'm coding a couple of approaches to make a list of checkboxes. My example program lets people select foods they like from a list. Changes to the interface are stored in @foods, and changes to @foods are reflected back to the interface. My goal is to write code that looks nice without writing too much.

Hopefully the insights from this will either make me better at using gui-easy, or will help gui-easy become easier to use.

First attempt:

#lang racket
(require racket/gui/easy
         racket/gui/easy/operator)

(struct food^ (name checked?) #:transparent)

(define/obs @foods `((1 . ,(food^ "Apple" #t))
                     (2 . ,(food^ "Banana" #f))
                     (3 . ,(food^ "Broccoli" #f))
                     (4 . ,(food^ "Ice Cream" #t))))
(obs-observe! @foods println)

(render
 (window
  #:size '(250 250)
  (list-view
   @foods
   #:key car
   (λ (k @food)
     (define name (@food . ~> . (λ (food) (food^-name (cdr food)))))
     (define checked? (@food . ~> . (λ (food) (food^-checked? (cdr food)))))
     (checkbox
      #:label name
      #:checked? checked?
      (λ (checked?)
        (<~ @foods
            (λ (foods)
              (dict-update foods k (λ (food) (struct-copy food^ food [checked? checked?])))))))))))

;; C-x C-e this to toggle a checkbox: (@foods . <~ . (λ (foods) (dict-update foods 2 (λ (food) (struct-copy food^ food [checked? (not (food^-checked? food))])))))

(My style is to use ^ to notate struct definitions.)

Things I don't like about this:

  • Despite all the line breaks through the program, there's still barely enough horizontal space for that final line and no good place to break it.
  • Inside list-view, each element of @food has to be extracted separately so it can be used in the checkbox. (I've encountered a similar frustration with list-view in other project.)
  • @food includes the key as its car, but I already have the key in k. If the key was excluded from @food, I wouldn't need to cdr, so the next line could be shortened down to (define name (@food . ~> . food^-name)), which is much better. (Though this wouldn't matter if the prior point could be solved directly.)
  • Having @food already available, but then having to dict-update on foods, adds more code. It would be nice if there was a shortcut to update @food itself. (This can't be done directly because it's derived, but maybe some kind of helper...?)

Second attempt:

#lang racket
(require racket/gui/easy
         racket/gui/easy/operator)

(define/obs @foods `((1 . "Apple")
                     (2 . "Banana")
                     (3 . "Broccoli")
                     (4 . "Ice Cream")))
(define/obs @foods-checked (set 1 4))
(obs-observe! @foods-checked println)

(render
 (window
  #:size '(250 250)
  (list-view
   @foods
   #:key car
   (λ (k @food)
     (define name (@food . ~> . cdr))
     (define checked? (@foods-checked . ~> . (curryr set-member? k)))
     (checkbox
      #:label name
      #:checked? checked?
      (λ _ (@foods-checked . <~ . (curry set-symmetric-difference (set k)))))))))

;; C-x C-e to toggle some checkboxes: (@foods-checked . <~ . (λ (fc) (set-symmetric-difference fc (set 1 2))))
  • Less code and shorter lines overall!
  • Storing the @foods-checked status separately from @foods makes it much easier to extract and update the relevant properties inside list-view.
  • Extracting the name is easier: (define name (@food . ~> . cdr)) (though could be easier still if it was unpacked for me)
  • Minor inconvenience of having to join @foods and @foods-checked together in order to use them fully.
  • Something rubs me the wrong way about ignoring the argument to the checkbox action function.

Overall I think there's still some room for improvement here, both in my code and in gui-easy, but I don't know what to change in order to improve it. One idea that I think has potential is if there was a version of list-view that included pattern-matching in order to unpack the list items for me. For example:

(define/obs @items '((1 "Red" "#ff0000" #t) (2 "Green" "#00ff00" #f)))
(list-view/match
 @items
 #:key car
 [(list k name hex checked?)
  (hpanel (text name #:color hex) (checkbox #:checked? checked?))]

or

(struct color^ (id name hex) #:transparent)
(define/obs @items (list (color^ 1 "Red" "#ff0000" #t) (color^ 2 "Green" "#00ff00" #f)))
(list-view/match
 @items
 #:key color^-id
 [(color^ k name hex checked?)
  (hpanel (text name #:color hex) (checkbox #:checked? checked?))]

But that's just a theory. Do you have any thoughts or ideas on my long-winded post?

As always, keep up the great work :)

@Bogdanp
Copy link
Owner

Bogdanp commented Jun 3, 2023

Re. your first example, I would extract helper functions to help reduce the indentation:

#lang racket/gui/easy

(require racket/dict
         racket/function)

(struct food^ (name checked?) #:transparent)

(define (set-food^-checked? f checked?)
  (struct-copy food^ f [checked? checked?]))

(define (update-food foods k checked?)
  (dict-update foods k (curryr set-food^-checked? checked?)))

(define/obs @foods
  `((1 . ,(food^ "Apple" #t))
    (2 . ,(food^ "Banana" #f))
    (3 . ,(food^ "Broccoli" #f))
    (4 . ,(food^ "Ice Cream" #t))))
(obs-observe! @foods println)

(render
 (window
  #:size '(250 250)
  (list-view
   @foods
   #:key car
   (λ (k @id+food)
     (define @food (@id+food . ~> . cdr))
     (checkbox
      #:label (@food . ~> . food^-name)
      #:checked? (@food . ~> . food^-checked?)
      (λ (checked?)
        (@foods . <~ . (curryr update-food k checked?))))))))

Re. the match idea: I can't think of a way to write a match expander that produces derived observables, so I don't think that can work with regular match. I don't have any better ideas about how to further improve this code at the moment, either.

@cloudrac3r
Copy link
Contributor Author

Thanks for the reply! You're right, extracting those food operations to functions does make it easier to read and write the code. In the future, I might play around with making a limited version of list-view/match which just covers unpacking lists and structs, since those are the only data types I've found myself using in list-views.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants