Fearless extensibility: Capabilities and effects

Thinking more about my IF variable-binding issue, it was to do with the quest to allow the user to represent knowledge in as simple a form as possible, with as little boilerplate repetition as possible. Using per-predicate (or per-function) variables seemed to require lots of repetition of those variables. Doing “the right thing” in terms of modern API design, violated “don’t repeat yourself” in a way I didn’t like.

For example, an action in an IF game often boils down to a bunch of context-specific variables: “actor” (the NPC who is doing the action), “verb” (the thing being done), “noun” (the object the verb is being done to), “second” (possibly another object involved in the action), etc.

To anchor this to the thread’s title: an “action” in an IF game might be a little like an “effect”. At least, in a very oldschool language like Inform 6, actions (if they’re run) pretty much do immediately change the state of the simulation, so they are super side-effectful.

If you define an action as a pure function or even a Prolog predicate - which is a nice declarative way to do it, and if you’re trying to use actions in AI type code where the NPC will want to reason about them without immediately causing changes in game state, you’ll want to be as pure-functional as possible - then you need to pass all of that context in on every invocation. And define those context variables every time. If as well as an “actor” you also want to pass in and return a “world” (a data structure representing either the world, or changes to it, for speculative AI-style planning), then it gets more and more complex.

Inform 6, being extremely 1970s-style old-school in its approach, just used global variables for things like “actor”, “verb”, “noun”. That turned out to be really terse and easy to understand! You don’t need to pass “actor” in as a parameter on every call. That doesn’t work if you’re trying to indirect an action for AI, but it suggests the part that’s intuitively missing: “implicit” parameters which don’t need to be defined when you’re defining the function, but only when you’re calling it. Since you’d be defining many times but only calling once, and the definitions would be user-facing (needing to be as simple as possble) while the call would be in library code.

Emacs, I think, has a similar problem / solution, where its extensions use brute-force old-school global variables or even dynamic binding.

But of course dynamic binding is super scary and prehistoric today - and probably has some really bad security properties.

That’s where my thinking stopped because it was a wall. But it is relevant I think to the problem of extensions. How do we get the upsides of dynamic binding without the security risks?

One possibility is maybe to pass one “context” parameter in on every function/predicate call, that contains an object with all those implicit variables.

Another way maybe is to use FEXPRs/vaus/macros, ie functions which take the entire calling environment as a parameter. That might though expose much more local/private context information than is safe, and the function definition still has to define that parameter.

Another approach - and perhaps the most terse, the least DRY-violating - is to avoid variables altogether, and instead pass around higher-order functions or (in a Prolog) higher-order predicates. These functions/predicates would have to take just one parameter, which could be a list/stack (as in Forth/Joy/Factor/Uxn) but might instead be an object/dictionary like structure. A “context” might be such a structure (it would have named components, so it would be more dictionary-like than stack-like). So eg for defining preconditions and effects of an action in a simulation (or for controlling a robot or a personal desktop agent of some kind), you might have a rule that takes and returns not quite a set of literal states and changes of the world, but rather functions/predicates over that world and/or context. (Where a context might include details about the action that the robot/agent/NPC is planning to do, not just the simulation/world itself). Then all the functions/predicates - the parts that a user might write - would have their name-binding happen at a very late stage, at the point where an AI doing some planning whips up a speculative imagined context, and passes that context to the planning function.

This last idea might be the sort of thing that Alan Kay is speculating about in his recent writing about objects. The extreme late-binding that Kay likes, is what appears when you start looking at AI-style planning like this.

However, I haven’t seen any actually-existing OOP environment, even Smalltalk, which makes using objects for knowledge representation simple and pleasant. There’s always (to my eye) still a lot of awkward redundant domain-irrelevant boilerplate in writing the method calls, which to me says we haven’t got to the core of the good idea yet.

Adding onto to @Apostolis response, effects are the domain of a problem being solved and effect handlers are one mechanism to address them.

Computational effects are basically just the formalism of side effects, or other cross cutting concerns that are interleaved in programs. E.g. non-determinism, state, logging. This is in some way similar to aspect oriented programming from OO where you have cross cutting concerns that are divorced from the primary business logic.

Effect handlers are a specific way to manage computational effects similar to exception handling with a resumable continuation. Like you noted this is very similar to common lisps condition system.

A lot of the modern focus is ensuring purity and typing these systems so they’re easier to reason about.

2 Likes

Sorry for the delayed response/read.

I also feel like capabilities and effects are both exciting tools for having intentional management for extensibility. I’m somewhat wary of the concept of marking every function call as an effect; I’m not positive but I think this is just implementing your own interpreter to intercept function calls. It seems like you’d have to do a careful job knowing what function it was you were intercepting. In the example both the color picker and palette are free variables so it’s also possible to consider that the level of interception by fully functorizing all dependencies like Newspeak so that you can dependency inject everything.

I talked about the distinction between open/closed extensibility in my post and it seems like this is most centered around modifying systems that are not built composable enough to have first class modification. If the original implementation is using an effect for either picking a color or adding colors to the palette, this would be a form of open extensibility where you could write a custom handler to transform the color at the handler level.

The larger issue is how you handle the case that the existing implementation is not factored in such a way that is amenable to you introducing the extension point you want. As an aside, I think this is the same issue that Kay was trying to solve with behavioral inheritance but you have the same issue with methods not being at the right granularity. One way in which I would like to be able to address this is by exposing the source program to a user and giving them behavior-preserving refactors that allow them to change the original program to be more amenable to extension without changing the original program. So in the color picker example even if the colorPicker.choose and palette.add functions weren’t already separated you could refactor the source into those programs and intercept on that level. There’s an open question on how amenable this would be to updates from the original author and dealing with conflicts.

1 Like