You can arrange data by its position or order, like an array:
set prizes = [ "Ferrari", "TV", "cornflakes" ] set yippee = "You've won the" && getAt( prizes, random(3) )Or, you can refer to it with labels, like in a C struct or Pascal record:
set things = [ #number : 17, #name: "LaVerne", #list: [1,2,3] ] set myNumber = getAProp(things, #number)You can add things to a list at the beginning, or in a specific location, or delete them or interrogate them according to a whole range of criteria. You can also sort them, count how many items they contain, and so on.
Because items can be added to them and extracted from them according to the values in variables, their content can be varied at runtime, so you can create complex collections of data that vary according to what the user chooses, or the colours of his/her monitor, or the phases of the moon. You aren't limited to the things you've actually placed in your scripts.
Lists are a good way of keeping track of things that might vary in number (such as which sprite channels are currently occupied), or of things that need to be linked together (such as a person's name and address).
They also provide a means of simplifying and generalizing complicated scripts by moving specifics out of the lingo code itself and into a single data structure that can be easily modified. Consider the following handler:
on mouseUp put getHotspot() into hotSpot if hotSpot = #noHotSpot then beep 1 else if hotSpot = #quitBtn then doQuit else if hotSpot = #helpBtn then doHelp else <etc> end mouseUpIf there are a dozen different possible hotspots this quickly gets out of hand. If, alternatively, you create a global list of hotSpots and the actions to take for each one, like this:
global gHotActions set gHotActions = [ #noHotSpot: "beep 1", #quitBtn : "doQuit", <cont> #helpBtn : "doHelp" <etc> ]Then the mouseUp script only needs to look up the correct action in the list, and do it:
on mouseUp do getAProp( gHotActions, getHotSpot() ) end mouseUpIf you want to add another hotspot, you just need to add it to the list. You don't have to change your scripts at all. You could even allow the user to define additional hotspots and assign actions to them, just by adding them to the list dynamically (though you'd also have to have a list defining which hotspots are which for use by the getHotSpot() function...).
In short, lists are extremely versatile and general structures which you can probably put to good use in almost any situation. And as if that weren't enough, they're also rather speedy.
A common cause of this error is not declaring lists as global. Lists only persist as long as the variables referring to them persist. If a list is defined in a local variable, and the handler it was defined in finishes, the list gets cleared away. If you then try to use it again without redefining it, you get this error.
Unfortunately, the external representation of lists and text is not especially distinct, and to make matters worse Lingo allows things to be placed in fields willy nilly without letting on that it's turning those things into blocks of text. When you say:
put myList into field myFieldIt looks as though the list is simply being placed, whole and unadulterated, into the field, but this is not the case. There's an implicit conversion taking place. The process could (and arguably *should*) be more explicitly written:
set the text of field myField = string(myList)Text is just a wodge of characters, and doesn't *mean* anything. When you do the reverse operation
put field myField into myListmyField's contents are, by definition, text, and so that's what goes into myList. Lingo can't know that you want the text interpreted as instructions to build a list unless you explicitly tell it so. If it just blithely assumed that "[a,b,c]" was the same as [a,b,c] then there'd be whole swathes of text that could lead to confusion; and if lingo always tried to interpret every piece of text it came across it would soon get bogged down.
So, if you have stored a list into a field, to retrieve it you must use the value() function. This tells Director to interpret the text as a Lingo data description:
set myList = value(the text of field myField)And more generally, try to be aware of the different types of data you're using, and why the differences are important. Just because Lingo isn't always absolutely strict about typing doesn't mean you can just forget about it.
set originalList = [ 1, 2, 3 ] set copyList = originalList add copy, 4 put originalList -- [ 1, 2, 3, 4 ]If you want to make a duplicate copy of a list containing only numbers, a quick way is to do this:
set copy = originalList * 1The copy thus created will point to a new set of items all its own rather than to the same set as originalList, and the two lists can then be modified independently.
Why does this work? Well, arithmetic operations applied to lists map across the whole list, and produce a new list as a result:
put ( 2 * [ 1, 2, 3 ] ) -- [ 2, 4, 6 ]Multiplying by one is an identity operation that leaves the content values unchanged, but still generates a new "results" list.
However, if you apply this to items which *aren't* integers, they'll mostly be treated as if they are (since all data, more or less, is internally represented by numbers), and your data will get mangled:
put 1 * ["abc", #blah, birth(script "blah") ] -- [11374992, 534, 0]A more general way to duplicate a list would be to use a handler like:
-- handler returning a duplicate copy of a linear list
-- this version recursively calls itself to copy items in aList
-- that are themselves lists, so these too will be duplicates
-- child objects in the list will still be shared by both the source
-- and duplicate lists, however, since there's no sensible way to
-- duplicate objects of unknown provenance (unless you provide
-- all your parents with their own "duplicate" methods...)
on duplicate aList
if not listP( aList ) return aList
set retList = []
repeat with x in aList
if listP(x) then add retList, duplicate(x) else add retList, x
end repeat
return retList
end duplicate
-- this is movie script "stack object"
property stack
on birth me
set stack=[]
return me
end birth
on push me,entry
add stack,entry
return entry
end push
on pop me
if count(stack)>0 then
set entry=getLast(stack)
deleteAt stack,count(stack)
return entry
else
return #empty
end if
end pop
-- additional stack operations can also be defined in such a script
-- (and Kurt did) but for the present let's keep to the basics
To create a stack you'd use:
put birth(script "stack object") into myStackYou can then push & pop from it by simply saying:
push myStack, myValue set popValue = pop(myStack)If you don't want to use parent scripts & objects, you can also create abstract data types in a more traditional functional fashion. The difference in this case is minimal:
-- non-object version of the stack data type
-- this goes in a movie script
on newStack
return []
end newStack
on isEmpty stack
return count(stack)=0
end isEmpty
on push stack, item
add stack, item
end push
on pop stack
if count(stack)>0 then
set entry = getLast(stack)
deleteAt stack, count(stack)
return entry
else
return #empty
end if
end pop
Useage would be the same, except that to create the stack you'd use:
put newStack() into myStackObjects have the advantage that they carry their methods with them wherever they go, and could for instance be passed to another movie in which the stack handlers themselves are not explicitly defined. They are also more enclosed, so there is less risk of confusing the stack data type with its implementation in Director lists (as an example of why this is desirable, consider the results of mistakenly "sort"-ing your stack).
Bearing this in mind, the next data type examples will be given using objects only. (For more info on parent scripts and child objects, see section 14.)
-- NB: this is an algorithm, not a legal Lingo statement!
tableEntry (row, column) -> listItem (column + row * rowWidth)As a more general solution, a table of arbitrary size can be created as a list of lists:
-- parent script "table object"
property table
on birth me
set table = []
return me
end birth
on setValue me, row, column, val
-- allow for zero indices
set row = row + 1
set column = column + 1
-- check to see if we've already created the appropriate row
-- and if not, do so
set thisRow = 0
if count(table >= row) then put getAt(table, row) into thisRow
if not listP(thisRow) then
set thisRow = []
setAt table, row, thisRow
end if
-- set our new value at the column position setAt thisRow, column, val
end setValue
on getValue me, row, column
-- allow for zero indices
set row = row + 1
set column = column + 1
-- check to see if the row is defined
if count(table >= row) then
put getAt(table, row) into thisRow
if listP(thisRow) then
-- check to see it the column is defined -- and if so, return it
if count(thisRow >= column) then
return getAt(thisRow, column)
end if
end if
end if
-- otherwise, return an empty value, which we'll
-- default to 0, because that's the default list item value too return 0
end getValue
-- again, you could add other table operations here
-- note that unlike the stack, this table will only ever get bigger,
-- never smaller...
I'll leave it to you to devise a table of arbitrary size including indices
less than zero...
Anyway, here's an alternative (rather outlandish) approach allowing arrays of arbitrary dimension (maybe they're trees really, but whatever...)
-- parent script "array object"
property array
on birth me
set array = []
return me
end birth
on setVal me, coordList, val
-- check we've got a proper set of coordinates
if (not listP(coordList)) then return #badCoord
if count(coordList) = 0 then return #badCoord
-- if there's only the one coordinate, set that value in our array
if count(coordList) = 1 then
setAt array, getAt(coordList, 1), val
-- otherwise, either create a new sub-array at our coordinate or use one
-- that's already there, and get it to set the value at the place
-- specified by the rest of the coordinate list
else
-- identify our coordinate, and delete it from the list
set thisCoord = getLast(coordList)
deleteAt coordList, count(coordList)
-- see if there's an array there already
set subArray = 0
if count(array) >= thisCoord then set thisVal = getAt(array, thisCoord)
-- if not, create one
if not objectP(subArray) then
put birth(script "array object") into subArray
setAt array, thisCoord, subArray
end if
-- do the recursive call
setVal subArray, coordList, val
-- rebuild the coordinate list
add coordList, thisCoord
end if
-- return a value to say that it worked
return #ok
end setVal
on getVal me, coordList
-- check we've got a proper set of coordinates
if (not listP(coordList)) or count(coordList) = 0 then return #badCoord
-- if there's only one coordinate, return the value of that in our array
if count(coordList) = 1 then
put getAt(coordList, 1) into thisCoord
if count(array) >= thisCoord then return getAt(array, thisCoord)
else return 0
end if
-- otherwise, check that there's an object at thisCoord, and summon the
-- value from it, or return 0 for undefined
put getLast(coordList) into thisCoord
if count(array) < thisCoord then return 0
put getAt(array, thisCoord) into subArray
if not objectP(subArray) then return 0
deleteAt coordList, count(coordList)
-- do the recursive call
put getVal(subArray, coordList) into retVal
-- repair the coordList
add coordList, thisCoord
-- et voila
return retVal
end getVal
To create your array, use:
put birth(script "array object") into myArrayand get and set values with:
setVal myArray, [1,2,3], blah put getVal(myArray, [1,2,3]) into blah(note: while this allows the creation of arrays of arbitrary dimension, it's important to always use coordinates of the same dimension within any single array. eg, setting x[1] = 1 and then x[2,1] = 2, you'll lose the value of x[1], because it will have become an array to contain all values x[n,1])
However, this is not always guaranteed. While Director's memory reclamation is often remarkably good, there are some cases it cannot cope with. In particular, if a list contains a reference to itself, the memory it occupies cannot be recovered:
-- pathological memory-leak example
-- note that this is specifically designed to devour a chunk of
-- Director's memory partition
-- in low memory conditions this
-- may cause your computer to hang, and even if it doesn't you
-- should certainly quit and restart Director after running it
on smallLeak
-- create a long list
set tempList = []
repeat with i = 1 to 10000
add tempList, i
end repeat
-- add a circular reference
add tempList, tempList
end smallLeak
on bigLeak
put "freebytes before = " & the freebytes set fb = the freebytes
-- throw away some memory a bunch of times
repeat with i = 1 to 50
smallLeak
end repeat
set fb = fb - the freebytes
put "freebytes after = " & the freebytes put "bytes lost = " & fb
end bigLeak
If you do actually run this example, check and purge the memory partition
in the "About Director" before and after doing so. Notice anything
worrying?
It isn't likely that you'll want to use such a blatantly circular data structure as the above, but even in less extreme cases circularity may arise if you're using complex structures. Since an ongoing memory leak can wreak havoc with the performance of your program, and may eventually cause a crash, it is important to be careful about cleaning up.
For most everyday purposes, however, lists can be left to dispose of themselves. If you know, for example, that your list only contains integers, there is no chance of self-reference and simply saying:
set myDisposableList = 0will dispose of it. Even this is only generally necessary in the case of global lists, which may otherwise persist indefinitely. Lists only used local to a particular handler should disappear when the handler completes. [A method for aggressively disposing of lists and objects is discussed in section 14.10]
Director Web:
Director FAQ [12]: Lists
HTML by Zac Belado, zac@wimsey.com
The Internet Connection at MCLI is
Alan Levine --}
Comments to
levine@maricopa.edu
URL: http://www.mcli.dist.maricopa.edu/director/faq/faq12.html