Clean way to implement a list of checkboxes?
cloudrac3r opened this issue · 2 comments
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 itscar
, but I already have the key ink
. If the key was excluded from@food
, I wouldn't need tocdr
, 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 onfoods
, 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 :)
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.
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.