Director FAQ || Notes ~ Short Index ~ Full Index ~ Search || Prev ~ Next ||

[12] Lists


Director FAQ [12] Lists

[12.1] What are lists good for anyway?

Lists allow you to collect and organize multiple data by various methods and treat the whole collection as a single item. A single list can contain items of different types, including other lists and child or factory objects.

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 mouseUp
If 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 mouseUp
If 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.

[top]


Director FAQ [12] Lists

[12.2] Why do I get a "handler not defined" error when using list operations?

List operations bring up this error when passed something that isn't a list. A function like GetAProp() is not defined for integers, for example - there's no possible right answer to the question "what's the fifth item in 1?"

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.

[top]


Director FAQ [12] Lists

[12.3] Why does my list stop being a list when I save it to a field?

Fields are strictly containers for text only. Anything placed in a field must be a piece of text. Lists, on the other hand, are a very different kind of entity. Lists may contain data of different types in their native form: they may be numbers, text, symbols or (of course) other lists. The distinction between these types is very important for the way lists behave and the way they are represented internally.

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 myField
It 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 myList
myField'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.

[top]


Director FAQ [12] Lists

[12.4] How can I make a duplicate copy of a list?

Perhaps confusingly, a list "value" kept in a variable isn't the data contained in the list items themselves, its just a pointer telling Director where those items can be found. As a result, if you assign it to another variable, both variables contain the same value, pointing to the same set of items, rather than keeping separate copies of all the items. If you change the items in one copy, all the other copies will also be changed:
  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 * 1
The 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

[top]


Director FAQ [12] Lists

[12.5] How can I define a stack?

The best way to define abstract data types is to use parent scripts. As an example, this is from Kurt Cagle :
-- 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 myStack
You 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 myStack
Objects 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.)

[top]


Director FAQ [12] Lists

[12.6] How can I define a table?

A table of fixed dimensions can be easily defined by using a single list and converting coordinate pairs to single indices as follows:

-- 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...

[top]


Director FAQ [12] Lists

[12.7] How can I define a multidimensional array?

The above approach for tables can be extended to any number of dimensions, but it gets pretty unwieldy. I'd probably suggest using fixed-size arrays wherever possible (they don't have to be of *fixed* fixed size, you can specify width, height etc in parameters to the birth handler).

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 myArray
and 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])

[top]


Director FAQ [12] Lists

[12.8] How do I get rid of the damn things?

Lists are automatically garbage-collected when no references to them remain. In most cases, this means that clearing the variable referring to a list, or allowing it to go out of scope (ie exiting the handler in which a local variable was defined), should lead to the memory it occupied being reclaimed.

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 = 0
will 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]

[top]


Director Web: Director FAQ [12]: Lists

aricopa Center for Learning and Instruction (MCLI)
Maricopa County Community College District

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