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

Action Dispatcher: Actions On-Demand + Cooperative Ordering #19

Open
psionic-k opened this issue Nov 28, 2024 · 5 comments
Open

Action Dispatcher: Actions On-Demand + Cooperative Ordering #19

psionic-k opened this issue Nov 28, 2024 · 5 comments

Comments

@psionic-k
Copy link
Member

psionic-k commented Nov 28, 2024

A dispatcher will interpret calls to actions before actions exist. This saves us from having to create actions. It can also solve interesting coordination problems.

#+begin_src elisp
(message "I am first")
#+end_src

#+attr_dslide_macro: [111 105 97 119 106 101 102 32 32 84 104 105 115 32 105 115 32 97 32 109 97 99
     114 111]

In the current Dslide, these will occur out of order if you order the actions incorrectly. There is no way to have them in order going both forwards and backwards because actions are called in reverse order when going backwards.

Keyword Registry vs Longer Keywords

To simplify markup and only hydrate actions as needed, the dispatcher just needs to be configured with an affiliated keyword registry. Propertize would use #+attr_dslide_propertize: and images would use #+attr_dslide_image: etc.

The alternative is to use

#dslide: action-class :config-1 value :config-2 value '(remaining stuff like keyboard macros) '(and other data as needed)

Where action-class is letting us know what to dispatch to already. This was decided against because actions would have to read the value into a plist instead of just looking at the keyword when deciding which elements to map over.

Quoting & Evaluation

Function calls may be encoded in the configuration line. To support this, we would want to require quoting of lists. Vectors also differentiate lists of data versus intent to evaluate an expression at runtime.

Indeed, babel does evaluate quoted expressions for parameters. It seems we are to treat unquoted forms in org as liable to be evaluated (a bad idea if you ask me, but it is what it is).

The Slide is the Dispatcher?

Just like how the babel blocks are labelled with directions and called in order, the dispatcher should be able to alternate between actions, selecting the right action when going the right direction, to encode the forwards-backwards ordering in a decidable way.

The slide class might just become the dispatcher. It would make sense because slide's live on top of their actions. Currently, the slide is the dispatcher, just a rather limited one. Because state in dslide is anticipated to go away when slides go away, this design would completely make sense.

@psionic-k
Copy link
Member Author

The meaning of dslide-forward and dslide-backward is not a small point. While the basic formula, doing things until they cannot be done and then returning nil, remains unchanged, it is possible that actions would no longer search forward and backward. Searching behavior would live in the dispatcher. That is highly desirable because it makes actions simpler. The dispatcher would be in control of weaving the step order.

However, actions will need to be made compatible with this change. Actions will either need to obtain state or have it passed in. Passing it in would forever bind caller and callee. For an interface as important as dslide-stateful-sequence, that is a bad idea. Leaving it in pre-arranged locations is more flexible, filthy, and satisfying. Actions do not own the dispatcher. They belong to it. They must "discover" frequently needed information and have it arranged for them to find. The dslide-parent is perhaps going to come into play. By always querying from the dslide-deck, we ensure that every action could (but likely will not) make decisions based on hierarchy, not just the immediate context.

The dispatcher will use a registry to search. This is because it cannot instantiate actions in order to ask them to search. Queries will live in the registry and the class instantiation may play some role, but more likely, let's just append the queries to a list, dslide-dispatch-specs or something. If a query key is hit, we instantiate an action and begin calling its methods.

Some actions do care about multiple elements, such as the list item reveal action. This is a reason for actions and dispatcher to be as agnostic as possible about who is driving. Legacy style actions will work. The APIs will only get simpler as it is convenient rather than by requirement.

This will be buried in methods that the user is not annoyed by and simply relies upon, but it is worth me at least thinking about. Such details find their way into the manual and code comments.

@psionic-k
Copy link
Member Author

I'm part-way through the implementation.

Actions like the hide-markup action only use begin and end methods. They don't really have any specific trigger. In the action registry, their match rule will simply be t. This also means there's no good place to attach data to them. At most, an affiliated keyword may tell them not to operate on some element. For example:

#+attr_dslide: hide-markup :ignore t

Actions do support class registries via the usual static method implementation. This can help the dispatcher without new classes requiring to be inserted into a list manually all the time. They just need a static slot or method for the search key.

The question I have for the dispatcher is how to differentiate an action's slots versus its configuration for a particular element. It would feel irritating if we cannot configure an action for an entire slide, but such configuration feels like it should belong in the property drawer, which has a problem:

The current form of :DSLIDE_ACTIONS: is basically a flattened alist, and I don't like that because a flattened alist is ambiguous to parse and kind of a horrid thing that only exists because Org mode doesn't have multi-line properties so that I can write non-flat alists. TBH the property drawer is a major downer and should have been re-done to be more lisp-like from the beginning of time, but that's another story.

A reasonable compromise is to search for property keys like DSLIDE_HIDE_MARKUP, but I do feel like this is confusing unless the keys used for affiliated attributes and standalone keywords also have their class appended as a suffix.

There are two reasonable choices of configuration pattern for affiliated keywords and keywords, but only one for properties. For properties, we probably want this:

* Heading
:PROPERTIES:
:DSLIDE_MACRO: :key value :key value
:END: 

For the corresponding keyword, we want either:

#+dslide: macro :key value :key value

or

#+dslide_macro: :key value :key value

And as we can see, the second choice is more consistent. We always write the class after the name. Well, the reason this sucks is because I have to decide what affiliated keywords and keywords and properties to look for. I cannot just pull the key and then make a decision. It does mean some extra useless calls.

The properties situation is the irritating one, but if I only use one key, then all actions in this transition phase have to watch for all keys. That about decides in favor of DSLIDE_MACRO and dslide_macro style naming everywhere.

This also means that ignoring an element can be targeted at just one action, such as #+attr_dslide_hide: :ignore t

I think prefixing is the way things are going. Unfortunately I need to change directions a bit.

@psionic-k
Copy link
Member Author

Actions should try to be blindly cooperative. We want any arbitrary amount of progress on a single element, but we probably don't need actions to jump around every element in the heading except during dslide-begin, dslide-end, and dslide-final. Those methods are about lifecycle, not about handling steps. The dispatcher should be able to walk through elements and call actions when it encounters a matching element.

The idea that two actions work on the same element is kind of rare and will remain rare, and edge case. To handle it, we can use the affiliated keyword order:

  • If there is no keyword, the implicit actions will operate in order (or reverse order when going backwards) that they are configured in the dispatch rules.
  • If there are more than one affiliated keyword, the outermost keyword (top) goes first when forward and last in reverse.

Reversing is not a trivial problem. There are two very common and very necessary reversal behaviors. Sometimes we undo behavior on the previous element. Sometimes we go back to the previous element. The image action does both depending on its configuration.

A problem I'm having to consider more deeply is that actions currently are not told where to attempt progress. Because of this, there is no equivalent to "peeking" to see if an action could do something. The action will simply move onto the next element 😱

I am concerned that the most obvious conclusion requires a change to dslide-stateful-sequence or at least to dslide-action.

To boil down the problem, traversing org elements and reversing over them by guessing the appropriate action, which is not decidable, requires reacting to whether the chosen action made progress. Actions currently cannot peek. Making the return values more complex is fragile, implicit communication. Making the input values more complex requires changing signatures.

@psionic-k
Copy link
Member Author

psionic-k commented Dec 20, 2024

⚠️ not done cooking. I started work on this and just want to leave it here. I have some outstanding questions...

  • hide markup type might re-use filter config as pretty much its entire config, but filter config is mostly about progress, and hiding markup only operates on begin end
  • argument overrides on element affiliated keywords have to re-configure that action's behavior right there, which would make actions more complex than they need to be. The dispatcher might take care of setting / unsetting these slots for each action.
  • needs for macros for faster action writing are really unclear

What kinds of matching rules must be expressed

We have two kinds of progress and some actions that only work on every element at begin and end. Many actions that work element-by-element also initialize by working on every element they match. At a minimum, there are independent matching rules for begin / end and forward / backward. However, if actions just pull the parse data for the current heading from a location left by the dispatcher, those that do begin / end don't need any help. It's forward & backward that require coordination, so that is also the case where the dispatcher needs a hint about when to call the action.

A match configuration tells us if we need to make an action and how we need to try it when attempting to make progress.

Proposed matching config structure:

(FOT PROGRESS)

FOT is "function or types" and can be a predicate function or a type or list of types (or t to match everything). PROGRESS is probably standard (default when nil) or reverse-in-place.

Child actions require no such hand holding because even the dslide-slide-action-every-child action follows the tree structure; there is not an intermingling of child action steps. They just have to cooperate with the parent action through their configuration. There is always one child action that can work on a heading, although I have found cases where I want to override the child to make it show itself independently even though it's structurally connected to a parent that wants to show it inline.

Having any match in the heading is what tells the dispatcher to instantiate the action so it can call begin or end. This suggests any such match configuration has to be discovered by the dispatcher on the heading; if it was discovered due to an affiliated keyword configuration, we would know the action is needed because of the keyword already.

I don't want matching to be as crazy as font-locking, but it should be configurable. Since EIEIO is a bit foreign to many users, having both a configured function or a class method to provide a default seems right.

Proposed dispatch config

This combines default action arguments and match config

(setopt dslide-action-defaults
        `((image dslide-action-image nil (#'custom-link-filter reverse-in-place))
          (babel dslide-action-babel (:hide-results t))
          (item dslide-action-item (:reveal t :inline t :direction begin)
                (#custom-match-function reverse-in-place))
          (propertize dslide-action-propertize)
          (hide dslide-action-hide-markup nil (planning . ,dslide-hide-markup-types))
          (kmacro dslide-action-kmacro :speed 0.04 :jitter 0.5)))

Each value is (SHORT CLASS CONFIG MATCH)

  • SHORT the short name for the class. It turns into the :DSLIDE_<SHORT>: property for drawer based config or #+attr_dslide_<short> and #+dslide_<short>: for the keyword based.

MATCH is (FOT PROGRESS) from above.

(cl-defmethod dslide-match ((obj action) element)
  "Return ELEMENT if this action will operate on it."
  element)

The slide action config will be only slightly different. We always use the :DSLIDE: property on the heading, so the value can just be SHORT-NAME . CONFIG and there is no matching configuration

Cooperative Argument Passing

For the most part, if progress tracking is implicit, they will only need to handle a single bound argument, element and return non-nil to indicate progress. Not every action will need this. Whatever work is done all the time, I can simplify with macros. Something like this:

dslide-define-forward
dslide-define-backward
dslide-define-begin
dslide-define-end
dslide-define-final

Some data like the org element parse for the current heading and the currently matched element could be bound in the body for fast lookup, avoiding re-parsing. The contract though is that the action has to update this parse data if it makes a change.

@psionic-k
Copy link
Member Author

I'm considering this mixed style of markup again:

:DSLIDE: class :key val :key val
:DSLIDE+: class :key val :key val
#+attr_dslide: class :key val :key val
#+dslide: class :key val :key val

As progress tracking logic becomes centralized, this is the easier way to do things and it's just more consistent to users.

Thanks to Ihor for pushing me to understand the property+ syntax for the property drawers.

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

1 participant