Bogdanp/racket-gui-easy

Make custom observables (e.g., parameters)

benknoble opened this issue · 3 comments

I have a use-case where I would like to turn Racket parameters into observables. I can wrap the parameter in an observable, and then manage all the patterns of using it myself, but I managed to extract those patterns. The trouble is, I can't create my own thing and tell racket/gui/easy that it should count as observable and how it behaves under all the observable operations. The best I can do currently is provide a set of macros that I call obsp for observable parameter. Now the management is hidden away, but I still have to remember the right thing to call (<~ or <~p?).

The fundamental reason for this is that my custom observable parameter is really three values: the parameter itself, an observable of the parameter, and a derived observable that uses the parameter's value. This third one is what we are usually concerned with when reading, and the value we want to use when writing via an update function, but we can only do the writes through the second value.

If there were a prop: or gen: interface I could implement on a struct and have it co-operate with all of racket/gui/easy's observable procedures, that would be my ideal world. As it is, here's the set of macros for observable parameters (they essentially hide the second value from you, though it is accessible; in my examples, it would be @internal-@op).

#lang racket

(require racket/gui/easy
         racket/gui/easy/operator
         syntax/parse/define
         (for-syntax racket/syntax))

(define-for-syntax (@internal stx id)
  (format-id stx "@internal-~a" id #:source stx))

(define-syntax-parse-rule (define/obsp n:id p:expr)
  #:with @internal (@internal #'n (syntax-e #'n))
  (begin
    (define/obs @internal p)
    (define/obs n (@internal . ~> . (λ (the-p) (the-p))))))

(define (internal-obsp-update! internal o f)
  (internal . <~ . (λ (the-p) (the-p (f (the-p))) the-p))
  (obs-peek o))

(define-syntax-parse-rule (obsp-update! o:id f:expr)
  #:with @internal (@internal #'o (syntax-e #'o))
  (internal-obsp-update! @internal o f))
;; don't need p~>/obsp-map because ~>/obs-map already work
(define-syntax <~p (make-rename-transformer #'obsp-update!))
(define-syntax-parse-rule (:=p o:id v:expr) (<~p o (const v)))
(define-syntax-parse-rule (λ:=p o:id f:expr) (λ (v) (:=p o (f v))))
(define-syntax-parse-rule (λ<~p o:id f:expr) (thunk (<~p o f)))

(define p (make-parameter #f))
(define/obsp @op p)

(obs-observe! @op (λ (x) (printf "op: Value: Got ~s~n" x)))

(equal? (obs-peek @op) (p))

;; bypass @op
(p 1)

;; new versions
(obsp-update! @op add1) ;; 2
(<~p @op add1) ;; 3
(:=p @op 4) ;; 4
((λ:=p @op add1) (obs-peek @op)) ;; 5
((λ<~p @op add1)) ;; 6

At least :=p looks cute…

I'm hesitant (for now) to provide an interface for observables because not doing so gives me more freedom to change the internals. For your use case, what do you think about using make-derived-parameter? For example:

(define/obs @v 42)
(define v-p
  (let ([local? #f])
    (obs-observe! @v (λ (_) (set! local? #f)))
    (make-derived-parameter
     (make-parameter (obs-peek @v))
     (λ (v)
       (begin0 v
         (set! local? #t)))
     (λ (v)
       (if local? v (obs-peek @v))))))

Your point makes sense; I can always go the route I mentioned.

I'm not sure I follow the derived parameter example, but it seems to be creating the parameter from the observable. I want to go the other way around, as I already have the parameters.

Ah, I see. I lost track of the fact that you're starting from a parameter.