auto-cleanup
Almost all uses of deferred evaluation are about cleaning up things that are dangling. Freeing memory, releasing contexts, closing files, etc. There's two kinds of 'thing' we need to consider when we want to clean up:
1) something on the stack that owns on to stuff on the heap
2) something on the heap
If we start with the first scenario we can well imagine auto-sending a message to the thing when the context is ending because once the context has ended it cannot exist anymore - it was on the stack after all.
[do-the-thing] [ -> ø |
bob: Person.
// compiler calls: bob release]
It gets trickier though when you have the thing aliased. Who owns what? for instance:
[do-the-thing] [ -> ø |
stuff: Map[String, String].
// force allocation to happen on the heap:
stuff["foo"] = "bar".
// alias stuff
other-stuff := stuff.
// compiler calls:
// stuff release.
// other-stuff release]
There's two answers here. The first being release should be hardened against extra unnecessary release calls - or; the second that another release call is an error and the compiler should recognise the alias and not release it.
The problem with detecting aliases is that secret aliases are possible; it's hard to know exactly when and where every variable, once you've called another method, is the same thing as before. It might have been modified in the other method in which case its internals may have been modified. You can no longer know you shouldn't free the old version and you should free the newer version.
[do-more-stuff: stuff]
[stuff: Map[String,String] -> Map[String,String] |
// force allocation (maybe)
stuff["bar"] = "baz"
stuff].
[do-stuff]
[-> ø |
stuff: Map[String,String].
stuff["foo"] = "bar".
other-stuff := do-more-stuff: stuff.
// compiler calls:
// stuff release
// other-stuff release]
Here we have no problem if the act of modifying the map in do-more-stuff also took a copy of its internals when we go to modify it. This was the intention for the language, the copy-on-write principle. We're not simply aliasing it, we are duplicating it.
[do-more-stuff: readonly_stuff]
[readonly_stuff: Map[String,String] -> Map[String,String] |
// compiler calls:
// stuff := readonly_stuff copy.
stuff["bar"] = "baz"
stuff].
[do-stuff]
[-> ø |
stuff: Map[String,String].
stuff["foo"] = "bar".
other-stuff := do-more-stuff: stuff.
// compiler calls:
// stuff release
// other-stuff release]
Now the two release calls are safe. We can rewrite this program to include all the code the compiler will write for us:
[do-more-stuff: readonly_stuff into: return_stuff]
[readonly_stuff: Map[String,String],
return_stuff: &Map[String,String]
-> ø |
return_stuff := readonly_stuff copy.
return_stuff["bar"] = "baz"].
[do-stuff]
[-> ø |
stuff: Map[String,String].
stuff["foo"] = "bar".
other-stuff: Map[String,String].
do-more-stuff: stuff into: other-stuff.
stuff release.
other-stuff release]
The default implementation of copy and release will do nothing:
[object copy] [Any -> ø | ].
[object release] [Any -> ø | ].
The implementation for something that holds on to another reference should be something like this:
[map copy] [in: Map → out: Map | {...in}].
[map release] [map: Map | map elements release]
Back at the top we also considered what happens if we have a reference to a thing. Well, by their very nature references are meant to be passed about and we cannot assume that they are released by the end of a context. If we want them to be managed they should be put in to a class type like Map or List that can be copied but points to contents on the heap.
This does beg the question though - when we release an object do all its variables also have release called on them? Yes. A Reference release call does nothing though, so if the Map contains a List and the List contains an Array and the Array has a memory slice then only Array needs to implement release.
It's a self solving problem in many respects; so long as the pattern is utilised correctly. A class that has a reference to another thing might need to implement copy and release. It may be that they must implement copy and release. We're going to learn that down the road but for now not require it.
With these automated but usually nothing calls under our belt we can finally write code without needing a bunch of deferred evaluation statements. This is perfectly safe code:
[do-stuff]
[ -> ø |
file: 'foo.txt' open-readonly.
buffer: Array[Byte, 4096].
file read-into: buffer]
We'd want to create something to hold on to a buffer if we want the buffer on the heap. For that we can use List:
[do-stuff]
[ -> ø |
buffer: List[Byte].
file: 'foo.txt' open-readonly.
buffer grow: file length.
file read-into: buffer]
Two dangling things. There may even be early exits and we'd be okay with that too. If we, on the other hand, did allocate something ourselves - we must release it and for that a deferred evaluation would be useful. But we're not going to implement it. Instead we might add a convenient class:
Resource :: {
#object-type: Class,
#release-method: #object-type -> ø,
object: #object-type}.
[resource release] [Resource -> ø |
#release-method evaluate: object].
[do-stuff]
[-> ø |
file 'foo.txt' open-readonly.
buffer: Resource[MemoryAddress, [buffer -> ø |
__context allocator release: buffer] =
{object: __context allocator allocate: file length}.
file read-into: buffer]
Wordy, but it does highlight the benefits of making simple 'wrapper' like classes to manage resources. Given you need only implement release to make your class act like one it's not too much of a burden.
With that, I think we can safely put deferred evaluation to bed. It need not exist in STZ. Unless we find that that is a mistake, it won't be coming back.