On Mon, Jun 10, 2019 at 8:57 PM Caleb Donovick <donovick@cs.stanford.edu> wrote:
First off, I have admittedly not read all of this thread. However, as designer of DSL's in python, I wanted to jump in on a couple of things I have seen suggested. Sorry If I am repeating comments already made. Over the last few years I have thought about every suggestion I have seen in this thread and they all don't work or are undesirable for one reason or another.
Glad to see I am not alone in the woods :-) So the argument that this problem is only Yanghao -- a single person's problem -- can be gone. You have to rephrase it to "this is only two person's problem" now. ;-)
Regarding what code will become simpler if an assignment operator was available. I currently walk the AST to rewrite assignments into the form I want. This is code is really hard to read if you are not familiar with the python AST, but the task it is performing is not hard to understand at all (replace assignment nodes with calls to a function call to dsl_assign(target_names, value, globals(), locals()). The dsl_assign function basically performs some type checking before doing the assignment. Once again the code is much harder to understand than it should be as it operates on names of variables and the globals / locals dictionaries instead of on the variables themselves. Also to get the hook into the AST I have to have use an importer which further obscures my code and makes use kinda annoying as one has to do the following: ``` main.py: import dsl # sets up the importer to rewrite the AST import dsl_code # the code which should morally be the main but most be imported after dsl ``` Granted anything in a function or a class can be rewritten with a decorator but module level code must be rewritten by an importer.
I thought about doing it a lot ... eventually manipulating AST is not so much easier than actually re-develop the entire DSL from scratch ... and this suffers eventually similar isssues like @=/L[:] overriding, it abuses a common understanding. I have also been looking into MacroPy3 for some time now, and despite you still need something like P[your customized expression] (the P[...] overhead which is exposed to end-users), changing an existing python syntax to mean something completely different, or translating a non-exist python syntax into something else really makes me feel this will make things mentally inconsistent, and actually makes python no longer python ... And this haven't touched what it means for debugging later on ...
The problem with overloading obj@=value: As Yanghao have pointed out @ comes with expectations of behavior. Granted I would gamble most developers are unaware that @ is python operator but it still has meaning and as such I don't like abusing it.
Yep.
The problem with using obj[:]=value: Similar to @ getitem and slices have meaning which I don't necessarily want to override. Granted this is least objectionable solution I have seen although, it creates weird requirements for types that have __getitem__ so it can just be used by inheriting `TypedAssignment` or something similar.
Exactly. I will try to summarize all the pros and cons I saw people posting for the L[:] case, and the thing for DSL is L[:] is confusing on its own (not better than obj.next = ...).
The problem with descriptors: They are hard to pass to functions, for example consider trying to fold assignment. ``` signals = [Signal('x'), Signal('y'), Signal('z')] out = functools.reduce(operator.iassign, signals) ``` vs ``` signals = SignalNamespace() signal_names = ['x', 'y', 'z'] out = functools.reduce(lambda v, name: settattr(signals, name, v)) ``` In general one has to pass the name of the signal and the namespace to a function instead the signal itself which is problematic.
The problem with exec: First off its totally unpythonic, but even if I hide the exec with importer magic it still doesn't give me the behavior I want. Consider the following ``` class TypeCheckDict(dict, MutableMapping): # dict needed to be used as globals """ Dictionary which binds keys to a type on first assignment then type checks on future assignement. Will infer type if not already bound. """ __slots__ = '_d' def __init__(self, d=_MISSING): if d is _MISSING: d = {} self._d = d
def __getitem__(self, name): v = self._d[name][1] if v is _MISSING: raise ValueError() else: return v
def __setitem__(self, name, value): if name not in self._d: if isinstance(value, type): self._d[name] = [value, _MISSING] else: self._d[name] = [type(value), _MISSING] elif isinstance(value, self._d[name][0]): self._d[name][1] = value else: raise TypeError(f'{value} is not a {self._d[name][0]}')
# __len__ __iter__ __delitem__ just dispatch to self._d
S = ''' x = int x = 1 x = 'a' ''' exec(S, TypeCheckDict(), TypeCheckDict()) # raises TypeError 'a' is not a int
S = ''' def foo(): # type of foo inferred x = int x = 'a' foo() ''' exec(S, TypeCheckDict(), TypeCheckDict()) # doesn't raise an error as a normal dict is used in foo ```
Python provides us all the flexibility to do all kinds of "fancy" things, so flexible that we can make it *NOT* look like python at all. I have done similar things for descriptors, and I am still thinking was it the right approach to make descriptor not behaving like the way a descriptor is supposed to behave.