A language feature that I really appreciate in Python is context managers. That’s what the with
keyword does— it starts a new context, setting up code that will be called before and after a block of code. The most common use is with resources, like opening a file. Without it, you might write
f = open('names')
names = f.readlines()
f.close()
However, being required to call functions in a particular order doesn’t make for a nice interface. Plus, what if an exception happens between open
and close
? Using a context manager fixes both of these issues:
with open('names') as f:
names = f.readlines()
The file will be automatically closed when execution leaves the with
block, even if it leaves by way of an exception.
In the project I’m working on right now, I had a pair of functions that always needed to be called in a particular order:
components = componentsOnStory(...)
Record the position of all ‘components’.- Now, the caller performs some action modifying the geometry of the story.
replaceComponents(components, ...)
Put the components back in the correct positions.
This is another example of a less-than-ideal interface where two functions must be called in a particular order. Javascript doesn’t have context managers, but we can approximate them with a function. Here’s what I came up with.
function withPreservedComponents(context, geometry_id, action) {
const components = componentsOnStory(context.rootState, geometry_id);
action();
replaceComponents(context, components);
}
And here’s what it looks like for the caller:
function moveFaceByOffset(context, payload) {
// ... some code omitted for brevity
const movedPoints = verticesForFaceId(face.id, currentStoryGeometry).map(
(v) => ({
x: v.x + dx,
y: v.y + dy,
}),
),
newGeoms = newGeometriesOfOverlappedFaces(
movedPoints,
// Don't consider face we're modifying as a reason to
// disqualify the action.
geometryHelpers.exceptFace(currentStoryGeometry, face_id),
);
// ... some code omitted for brevity
withPreservedComponents(context, currentStoryGeometry.id, () => {
newGeoms.forEach((newGeom) =>
context.dispatch("replaceFacePoints", newGeom),
);
context.dispatch("replaceFacePoints", {
geometry_id: currentStoryGeometry.id,
face_id,
vertices: movedGeom.vertices,
edges: movedGeom.edges,
dx,
dy,
});
});
}
I feel like this is an improvement, because the caller is no longer responsible for remembering to call the replaceComponents
function. The api of the library is smaller — I’m now only exporting the one withPreservedComponents
function.