Is there an order-preserving Environment quark?

Looking at the StoredList in the jitlib extension quark, it’s “close but no cigar” for me because it doesn’t extend Environment so there’s no easy way to use it as writable environment while preserving the original key order. So has someone written such an add-order-preserving Environment, in the vein of Python 3.7+ dict?

The usual way to do this is to maintain a separate collection with key order, for cases where you need to iterate over keys in a specific (e.g. insertion) order. AFAIK this is what Python’s dict does internally.
You should be able to subclass either Environment or EnvironmentRedirect to achieve this easily.

Actually it’s not so easy to achieve it seems. It was done/attempted in wslib’s OEM, but


e = OEM.new;

e.['foo'] = 2
e.['bar'] = 3
e.['zap'] = 4

e.use { ~foo + ~bar + ~zap } // ok
e.use { ~bar = 33 } // replaces ok, order preserved
e.use { ~moar = 5 }
e.use { ~moar } // actually stored

e // but NOT showing up in the ordered view; also
e.keys // doesn't have "moar"

The problem seems to be that Environment.use requires native/C++ magic in the form of currentEnvironment (which always seem to refer to an Environment object, not really any extended class), so it seems you cannot really extend that use method at sclang level. It’s probably not calling into the subclass put.

Actually

e.use { currentEnvironment.class } // -> OEM

So that’s somewhat more miffing.

I’m not sure how OEM’s use is implemented - it might incorrect. There’s nothing magic about currentEnvironment, it’s just a global slot that is used to resolve ~environmentVariables. The only requirement for something used as a currentEnvironment is that it has at and put methods that accept symbols, because ~foo = 100 compiles to currentEnvironment.put(\foo, 100).

d = Dictionary();

currentEnvironment = d;
~foo = 1;
~bar = 2;
~sum = ~foo + ~bar;
d.postln

Here’s the quickest possible implementation of ordering in an Environment. This could be extended more completely via subclassing, obviously.

(
var envir, order;
order = OrderedIdentitySet();
envir = EnvironmentRedirect(());
envir.dispatch = {
	|key, val|
	order.remove(key);
	order.add(key);
};

envir.use {
	~a = 1;
	~b = 2;
	~c = 3;
};

order.postln;
order.do {
	|k|
	"% -> %".format(k, envir[k]).postln;
}
)
1 Like

I can assure you it doesn’t work like that for classes derived from Environment. I’ve instrumented the trivial implementation of OEM with

+ OEM {

	put { |key, value|
		"In OEM put: % %".postf(key, value.cs);
		if( value.isNil ) {
			keys.remove( key );
		} {
			if( keys.isNil or: { keys.includes( key ).not } ) {
				keys = keys.add(key);
			};
		};
		super.put( key, value );
	}

}

And now

e.['foo'] = 2 // prints the debug message, whereas
e.use { ~moar = 5 } // does not

So clearly it’s not calling into the subclass (=OEM) put on the 2nd line. Also

e.use { ~foo = 65 }

doesn’t call into OEM’s put either. It just happens to work in the superclass directly because OEM only adds a key order side-array.

Looking at your code, dispatch might be the method that is missing.

Ah, overriding a subclass of Environment can be weird because the interpreter optimizes Symbol:envirPut for cases where currentEnvironment is an Environment, skipping your method override. This is what EnvironmentRedirect is used for.

1 Like

Also EnvironmentRedirect doesn’t extend Environment, but implements all the methods “all over again”, including use. So it’s “extension by 100% duck typing.” Probably because of that issue with the compiler optimization…

'tis no type but duck when s’colliding!

Yeah, well the not so amusing part is that EnvironmentRedict (although it’s from Jitlib) doesn’t work with EnvirGui also from Jitlib, because of an explicit check isKindOf(Dictionary) in the latter. And likewise for passing cleanup through it: EventStreamCleanup won’t add anything using the same test. So not quite 100% drop-in replacement, alas.

And probably why it works with Dictionary. In prSymbol_envirPut:

    if (!ISKINDOF(dict, class_identdict_index, class_identdict_maxsubclassindex))
        return errFailed;

@scztt Actually, I don’t quite understand why it has to always call the identity dict method a line later:

    if (!ISKINDOF(dict, class_identdict_index, class_identdict_maxsubclassindex))
        return errFailed;

    identDict_lookup(dict, a, calcHash(a), &result);

Why can’t it call the subclass’ method?

@RFluff
I have an implementation of an ordered IdentityDictionary that maybe you can base an implementation on?

2 Likes

Since you’re extending IdentityDictionary, I’m betting it doesn’t work properly with key setting via use; see discussion above.

There are two ways to extend a class:

  • Subclassing. This can be described as an “is-a” relationship: the new class “is” of the base class’s type.

  • Object composition – “has-a.” The new class owns an instance of the base class but isn’t a subclass. Its methods use the existing interface of the base class to implement new behavior.

Gang of Four Design Patterns recommends composition over inheritance, in general. They note that subclassing is often one’s first thought about extending a class, but may not be the best thought in every case (they go a bit further to suggest that subclassing is usually not the best).

I felt I should note this because EnvironmentRedirect’s approach is mentioned pejoratively here, when in fact it’s closer to Design Patterns than a subclass would be.

Failing .isKindOf(Dictionary) tests is bad, but GoF would argue the problem is with the test, not with object composition. We should be programming to interfaces, not to types. So you’ve found a concrete place where we really do need .isDictionary (which we had resisted implementing before, but EnvirGui does need it).

hjh

A type is an interface. You probably meant to quote the “program to interfaces, not implementations”".

I don’t know if GoF would really say that, but Guido van Rossum probably would (and Didier Rémy would even more certainly argue that). What you are saying here is that SC should prefer duck typing tests to nominal typing tests. I don’t know if GoF has a position on this.

In more appropriate CS/PL theory terms, duck typing is a structural type system as opposed to a nominative/nominal one.

Aside: I don’t think GoF talks about a duck typing equivalent in C++ teribly clearly (they drop the C++ template mention here and there to say that it can be used to avoid inheritance when implemnting this-or-that pattern), but Abrahams & Curtovoy have made a more clear point about that in their Template Metaprogramming book. But this is essentially type erasure in C++, a concept that GoF never actually got to making explicit. (And type erasure is only really a pattern if you want to embrace the GoF book criticism that it mostly proposes “manually expanded macros” that could mostly be addressed by better language features.)

After Python added its optional static type checker, they have struggled with the same problem, i.e. conflicting notions of type. Their solution (PEP544) was to invent a notion of “protocol” that duplicates duck typing at the static type checker level. (“Protocol” is what other statically typed languages would call interface, but protocols can be “slapped on top” without the base class having any idea about them.) But as it turns out, they wanted the “cool new thing” at runtime too in some cases:

@runtime_checkable decorator and narrowing types by isinstance()
The default semantics is that isinstance() and issubclass() fail for protocol types. This is in the spirit of duck typing – protocols basically would be used to model duck typing statically, not explicitly at runtime.
However, it should be possible for protocol types to implement custom instance and class checks when this makes sense, […] The most type checkers can do is to treat isinstance(obj, Iterator) roughly as a simpler way to write hasattr(x, ‘iter’) and hasattr(x, ‘next’). […]

The last bit could be a minimalistic way to do interfaces/protocols in SC too, sans a static typechecker, i.e. just check an array of methods exists.

~suppotsProtocol = { |obj, protoc| protoc.every {|m| obj.respondsTo(m)} };
~protocolMinimalDict = [ \put, \putAll, \at ]; // maybe some more
~suppotsProtocol.(Dictionary.new, ~protocolMinimalDict)

There’s of course not much functionality in this, but if it were “made official” it might dissuade the use of isKindOf when not strictly necessary. It would be a lot more work to decide a good set of protocols though, as the current lack of such a notion in SC entails that everything that’s implemented is the class’ protocol… (which does get us to the 1st quote in this post on interfaces vs implementations.)

I think, in general, yes. James McCartney more than once expressed a distaste for isKindOf checks. But it’s also true that the following pattern, for too many classes, would be method clutter.

+ Dictionary {
    isDictionary { ^true }
}

+ Object {
    isDictionary { ^false }
}

Dictionary is used fairly generally and has distinct characteristics. I’m not sure now why it didn’t make the cut for this.

hjh

1 Like

I managed to “fix that” without touching the (actually) offending C++ code (prSymbol_envirPut which does a “non-virtual call” into IDict), although my fix does look a bit inefficient:

+ Symbol {

	envirPut { arg aValue;
		//("envirPut hook for a" + currentEnvironment.class).postln;
		if(Event.superclasses.add(Event).includes(currentEnvironment.class).not,
			{/*"overrrided native".postln;*/ ^currentEnvironment.put(this, aValue)},
			{^this.nativePut(aValue)}
		);
	}

	nativePut { arg aValue;
		_Symbol_envirPut
		currentEnvironment.put(this, aValue);
		^aValue
	}
}