Why we should remove {} from Squeak

Richard A. O'Keefe ok at atlas.otago.ac.nz
Wed Oct 3 05:08:50 UTC 2001


"Dirk Wessels" <icircle at xs4all.nl> wrote:
	I thought that it might be interesting to have a
	"pre-evaluate"-option in Smalltalk.
	
	method1
	| arrayConstant classListConstant byteArray pi |
	Smalltalk preEvaluate: [
	  arrayConstant := {1 3 4 5 6} asIntegerArray. "converted only once!"
	  classListConstant := {OrderedCollection Collection SortedCollection Array}
	asArray.
	  byteArray:= {3 4 3 5 34 43 3 4} asByteArray.
	  pi:= (1.0 asin) *2.0.  "calculated only once!"
	].
	"the rest of the code"
	^pi.
	
Syntax aside (where are the separating dots in those arrays?),
that's indeed an interesting idea, except that the results are assigned	
to variables that are going to go away again.
	
Eiffel has a similar idea, called "once functions".

Normally you'd write a function like

   array_constant: ARRAY[INTEGER] is
      do
         Result := <<1, 2, 3, 4, 5, 6>>
      end

and every time you call that, the function is called and a new array
created.  But if you write

   array_constant: ARRAY[INTEGER] is
      once
         Result := <<1, 2, 3, 4, 5, 6>>
      end

then the result is calculated the first time the method is called,
and then cached, future calls just fetching the value.  This is very
like lazy initialization in Smalltalk, except that it does not rely
on having a reserved value.  The idea makes sense for any function,
but Eiffel restricts it to functions with no arguments.  The result
is not calculated if it is never needed.

What exactly does "once" mean?  Is it at most once per
    (1) program?
    (2) thread?
    (3) subclass?
    (4) object?
Lazy initialization in a normal Smalltalk method corresponds to (4),
lazy initialization in a class method to (1).  The SmallEiffel
mailing list had an extensive discussion of whether it should mean
(1) or (2) in multithreaded Eiffel, which tells you it's not (3) or (4).

I think it's reasonably clear that all four of these are useful.
For Smalltalk, we'd have to consider
    (a) Evaluated when the method is compiled, including loading
        from a change set.
    (b) Evaluated when the method is changed, saved as some kind of
        DoIt using an embedded ReferenceStream or something.
    (c...z)

We could get a choice between (3) and (4) by having a slightly
different form of method declaration:

<method name> '{' <inst var or class var>* '}'
    <usual method body>

The idea here is that there would be two hidden instance variables
associated with the method:  %value and %valid.  Both would initially
be nil.  Any assignment to one of the instance variables between the
curly braces would also generate an assignment of nil to %valid.
The body would act like
    %valid ifNotNil: [^%value].
Each ^<expr> in the body would become
    ^[%value := <expr>. %valid := true. %value] value
including the implicit ^nil at the end.

So Dirk Wessels' example would become
    arrayConstant {}
	^{1. 3. 4. 5. 6} asIntegerArray

    classListConstant {}
	^{OrderedCollection. Collection. SortedCollection. Array}

    byteArray {}
        ^{3. 4. 3. 5. 34. 43. 3. 4} asByteArray

    pi {}
	^1.0 asin * 2.0

    method1
        "the rest of the code"
        ^self pi

Both instance methods (4) and class methods (sort of 3) could use this
notation.

I rather like the automatic recomputation after a change to an instance
variable or class variable that the result is declared to depend on.

BUT

In the case of {}, the feature is already in the language.
In the case of preEvaluate:, or my cached methods, the feature is
NOT already in the language.  If it were, I would argue for retaining it.
But it isn't, and we have a Smalltalk-wide 'design pattern' for doing
without, called lazy initialization.  So I argue for keeping such
features out until the design alternatives are much better understood.





More information about the Squeak-dev mailing list