Clean way of "Postelizing" callbacks #34
thorwhalen
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Described here is a design proposal to balance explicitness and UX of callbacks (more precisely, callable arguments that parametrize the behavior of python objects). The scope of this design goes beyond
dol, but we'll usedolexamples, since this is where we want to solve the problem first.Context
At the time of writing this, wrap_kvs has some design problems. One of its syndromes is mentioned in the wrap_kvs inconsistencies (due to incorrect signature-based conditioning) issue, and is probably the cause of some other problems.
The source of this is some not-well-thought-out "postelization" of the input "transformation" functions. The code looks at the signature of the functions to try to decide on how to apply it.
Why is it that (see this issue), as arguments of
kv_wraps,obj_of_data=lambda x: bytes.decode(x)andobj_of_data=bytes.decodelead to different behaviors (the second one raises an error). In fact,obj_of_data=lambda self: bytes.decode(self)would lead to that same error too. All of these definitions ofobj_of_dataare functionally equivalent in that you get the same outputs for the same inputs, so we really shouldn't have a divergence of behaviors here.The problem is that
wrap_kvshas this horrible thing where it looks at the signature ofobj_of_dataand applies the function differently in both cases. More specifically, the default behavior is to doobj = obj_of_data(data), but ifobj_of_datahas at least two required arguments (i.e. have no default), or the first argument name is "self", is will instead use it asobj = obj_of_data(self, data)!Why does it do that? Because though the first behavior seems to be the most common, sometimes we need
obj_of_datato have access to the store instance (self) to do it's job (for example, seeingself.rootdirto use that in it's transformation.A better solution (and it's problems)
kv_walk on the other hand has a good "engineering" design. The signature is:
Check out the code and see how simple it is.
And yet,
kv_walkcan be parametrized to do pretty much any kind of nested-structure processing you can imagine.It is no doubt the best design as far as engineering goes. But what about the UX?
No doubt, such functions as
kv_walkare destined to be used in specialized functions that are more user friendly, but still, see that this raw interface obliges the user to expresspkv_to_pv"fully".Even if all that is needed is a function
foofrom, say,ptov, the user would have express it tokv_walkaspkv_to_pv=lambda p, k, v: (p, foo(p)), orwalk_filt=lambda p, k, v: foo(p)This is the problem that
wrap_kvstried to solve (badly) with it's conditional acrobatics.It tried to make it easy and natural for the user to use, for the most common use cases.
It's a noble cause to strive for a good development UX.
And it's not only about making it easy for the user: It has positive effects on the behavior of the system as well.
I wrote more about general problem of UX vs explicit compromise here, so won't repeat myself.
I will, how ever look into the particular problem of input functions/callbacks here.
Let's now see how we can make our better solution even better: Getting a better UX without sacrificing too much (if any) design robustness.
call forgivingly? Nah!
One solution would be to use i2.call_forgivingly.
This function allows you to extract the inputs a function needs via their variable names.
That is, doing
obj = call_forgivingly(obj_of_data, self=self, data=data)in the code ofwrap_kvswould enable anyobj_of_datato have access toselfanddata, extracting only what it needs from it.Still, this isn't the best from a UX point of view: The user's callbacks' argument names need to comply to the expected convention (have argument names
selfand/ordata)!It's also not great from a robustness point of view (what if a function used the argument name "data", but didn't mean that data (in the context of the
call_forgivinglycall).wrap in a binder
What if the user had at their disposal a class to wrap their function to give it the required single-stable interface (like the one
kv_walkhas).This resembles the
bindpart of a FuncNode or the InnerMapIngress (which we might actually want to use here).Essentially, we'd be able to do things like (but this is not my final interface for this!)
That's a worse UX than what we use in
kv_walk, and probably worse in robustness as well (more layers, less simplicity). But the set up opens the possibility of some desirable abilities.Things like having an object that encapsulates the concern of mapping external functions to the expected form for a callback.
Things like doing:
and then offer the
apply_to_phelper function to the user to use asapply_to_p(foo)instead of having to do thelambda p, k, v: foo(p)(which seems convoluted, especially if names are long, and is not picklable) every time.Still, I'm not convinced it's worth it...
Beta Was this translation helpful? Give feedback.
All reactions