Shirakumo / alloy

A new user interface protocol and toolkit implementation
https://shirakumo.github.io/alloy
zlib License
181 stars 12 forks source link

[Discussion] ideas for improving REPRESENT macro #3

Open Inc0n opened 4 years ago

Inc0n commented 4 years ago

I think the REPRESENT macro currently serves as the only and unified component initialization method, could use some work:

1. isolate data creation, perhaps move that to the user's side

introduce new functions: #'alloy:add-observable-callback for adding observable callbacks, #'alloy:set-data' for data setting, and corresponding new functions for each of type of data.

In each of the function we can reinforce validation accordingly, because #'expand-place-data and #'expand-compound-place-data already serve this purpose and exposed to the user, this would be just renaming some of the methods.

2. Considering remove the T case in 'represent-with'.

So that (alloy:represent "Hey!" T) becomes (alloy:represent "Hey!" 'alloy:label), this is would be much more consistent with the rest of the components. Doing this for symbol, nil & T, and maybe #'component-class-for-object can be removed?

Shinmera commented 4 years ago

represent is not intended to be the only way to construct things. You can make-instance components just fine. represent is supposed to represent data in the UI somehow, hence the name. In fact, if you already have a data object, represent will not be the right tool to use.

I don't understand what add-observable-callback is supposed to do. Is it supposed to allow the user to specify a function to be called when an observable is changed somehow?

I also don't understand what set-data is supposed to do. Set the data of what? Are you talking about the data encapsulation class? Why would there be a new function for each type? Are you talking about types of data, or protocol classes?

The T case for represent cannot be simply removed since it is important in cases where UI is constructed programmatically and it needs to be able to construct a component to represent some data without explicit user feedback.

You also can already simply write (represent "foo" 'label) if you want to be explicit. There's nothing stopping you from doing that.

Inc0n commented 4 years ago

I see that my ideas are still flawed and a bit muffled. I have the following code snippets which I think will better illustrate my ideas about add-observable-callback and set-data, the idea about data isolation as I called it, for now:

(let* ((data (make-instance 'data))
       (freq (alloy:represent (frequency data) 'alloy:ranged-slider))
       (phas (alloy:represent (wavephase data) 'alloy:ranged-slider))
       (amps (alloy:represent (amplitude data) 'alloy:ranged-slider))
       (plot (alloy:represent (lambda ((frequency (frequency data))
                                       (amplitude (amplitude data))
                                       (phase (wavephase data)))
                                ;; return a vector of plot data
                                )
                              'alloy:plot :y-range '(-100 . 100)
                              :style `((:curve :line-width ,(alloy:un 10))))))
  ;; code
  )

would be changed to:

(let* ((data (make-instance 'data))
       (freq (alloy:represent 'alloy:ranged-slider (alloy:data-source (frequency data))))
       (phas (alloy:represent 'alloy:ranged-slider (alloy:data-source (wavephase data))))
       (amps (alloy:represent 'alloy:ranged-slider (alloy:data-source (amplitude data))))
       (plot (alloy:represent 'alloy:plot
                              (alloy:make-observable-callback-source
                               ((frequency data) (amplitude data) (wavephase data)) ;; observables
                               (lambda (frequency amplitude phase) ;; callback
                                 (make-plot-data
                                  (let ((range (* 2 PI frequency)))
                                    (lambda (i)
                                      (* (sin (* phase range i))
                                         amplitude)))
                                  '(0 . 1000))))
                              :y-range '(-100 . 100)
                              :style `((:curve :line-width ,(alloy:un 10))))))
  ;; more code
  )

Each of the data passed into represent associated with the components are all clearly "labeled", by introducing the data initialization functions. Note I changed the names of the initialization function, to be more accurate.

I also don't understand what set-data is supposed to do. Set the data of what? Are you talking about the data encapsulation class? Why would there be a new function for each type? Are you talking about types of data, or protocol classes?

set-data would be the function to set the data source of the component after component initialization, for example: after initialized phas with (make-instance 'alloy:ranged-slider) we could set the data of phas with set-data-source with (set-data-source phas (phase data)) (Perhaps set-data-source is a better name than set-data). However, seeing bit more about the relation between components and data and component. I think that post-initialization data setting are not very inline with alloy. Perhaps this idea can be scrapped.

The T case for represent cannot be simply removed since it is important in cases where UI is constructed programmatically and it needs to be able to construct a component to represent some data without explicit user feedback.

Ahh, I see now, that would be some very handy feature! Hmm, in this case, giving T case a more intuiative name will be my suggestion at last. something like: 'find-component.

Shinmera commented 4 years ago

I'm afraid I still don't understand the purpose of your proposed changes. So instead, let me try and explain what the idea is behind the component and data protocols again, maybe that'll clear up some of the confusion I feel is floating around here.

In most other UI frameworks there is a disconnect between the data the system operates on and the data that the UI shows. For instance, a text input field will have its own text slot that contains the text to display. It is then up to the programmer to shuffle the data into the text slot and back out when changes occur in the system, or are made by the user.

This divide is necessary, as the languages these systems are written in, including Lisp, do not allow observation of changes made to standard data structures. In order to make this observation possible the system would have to poll, which may be prohibitively expensive. However, what can be done is to attempt and minimise this divide and define a standard protocol for managing it.

At the lowest level this is what the observables protocol is for. It defines a way to signal changes being made to an object, and a way to register functions to be called whet a certain change is made to a specific object.

The data protocol is supposed to perform two purposes: on one hand it should represent a contract between a component and the data it requires -- typically only a single value to represent, but sometimes more -- and on the other it should help to handle the observation of changes to standard lisp data types that cannot be observed standalone, and standardise the protocol that way.

So finally we come to represent. Ideally the user should construct their system internally to do whatever they need, and then be able to construct a UI for its data as easily as possible. This means that they should be able to just 'represent' their data in the UI with a single construct. In order for a component to be able to read out the necessary data, it needs to know how to do that, and the represent macro is supposed to figure that out for you, if at all possible.

The aforementioned divide however means that we cannot always have a smooth integration with the UI if the system itself changes the data, rather than the UI. This is where most of my displeasure comes from. I'm not sure if this means that my attempt at removing the divide between the UI and the backing data is wrong to begin with quite yet. However, I'm sure there's no way to completely remove it, so there will always be some amount of disconnect.

The case of the lambda for a plot is a bit of a weird one and honestly I think it should be completely removed from represent. The problem there is that the function that computes the values for the plot to show needs to be called whenever one of several values of the data it's computing its value from changes, so it's more of an intermediary product rather than a part of the data to show.

I hope that clears some stuff up.

Inc0n commented 4 years ago

Thanks for the explanation.

I still don't understand the purpose of your proposed changes My proposed changes were really syntactical.

I am still not clear with the following:

The aforementioned divide however means that we cannot always have a smooth integration with the UI if the system itself changes the data, rather than the UI.

by system do you mean having (setf (data of-example-component) to-some-other-data) within user code?

I agree with this:

The case of the lambda for a plot is a bit of a weird one and honestly I think it should be completely removed from represent.

That is why I thought it will be helpful have the alloy:add-observable-callback, perhaps not alloy:set-data:

(let* ((data (make-instance 'data))
       ;; other components
       (plot (alloy:represent 'alloy:plot
                              nil
                              :y-range '(-100 . 100)
                              :style `((:curve :line-width ,(alloy:un 10))))))
  (alloy:add-observable-callback
   ((frequency data) (amplitude data) (wavephase data)) ;; observables
   (lambda (frequency amplitude phase) ;; callback
     (setf (data plot)
           (gen-new-plot-data frequency amplitude phase)))))
Shinmera commented 4 years ago

I mean if you have something like

(let ((a 0))
  (alloy:represent a 'alloy:slider)
  (setf a 1))

There's no way for the slider to know that a has changed and for it to update its shown value to the one currently set in the system.

Inc0n commented 4 years ago

Okay, I dont think this is a flaw of the design of Alloy, or anything along that line. Like you said

the languages these systems are written in, including Lisp, do not allow observation of changes made to standard data structures.

Therefore, in order to make this the observation on said a, a must be observable in the first place. This is a situation that I don't think is avoidable.

The users need to use place-data to construct an observable data, for the changes to be observed. And to modify the content of a data programmatically, (setf value) is to be used. It seems perfect for me. I don't see this is a problem.

Shinmera commented 4 years ago

Well yes, it's unavoidable, but the question remains whether it's worth it to try and bridge the gap at all, or whether that will just confuse users needlessly and create annoying designs, since only some things will be immediately observable and others need to be handled specially, rather than just forcing the users to make the data interaction explicit like other frameworks do.