Can't delete item from custom associative list

I’m working my way through Hamish Anderson and Hanaan Rosenthal’s excellent Learn AppleScript. If you know a little about AppleScript but want to get a clearer understanding of what you’re doing and how to do it better, I highly recommend this book.

I believe I have found a flaw in one of the exercise scripts in the book, and I’ve spent a few days trying to figure out why it doesn’t work and how to fix it. I have failed.

At first I thought I had a typo somewhere in my script, but then I went to the book’s website and downloaded the code. The downloaded script gives me the same error as the one I had typed out myself.

It’s script 18-22 in the book. When I run it, I get this error:

Script Debugger highlights this line in the delete_associative_item handler:

set contents of record_ref to missing value

The script is below. What’s going wrong?



on make_associative_list()
	(* Make a new associative list that stores values using any objects as keys.

		Result: an associative list

		Note: Users should not directly manipulate the contents of the returned 
		object. Instead, use the handlers provided to work with it safely.
	*)
	return {class:"associative list", the_items:{}}
end make_associative_list


on find_record_for_key(the_assoc_list, the_key)
	(* This is a private handler. Users should not use it directly. *)
	considering diacriticals, hyphens, punctuation and white space but ignoring case
		repeat with record_ref in the_items of the_assoc_list
			if the_key of record_ref = the_key then return record_ref
		end repeat
	end considering
	return missing value -- The key wasn't found
end find_record_for_key


on get_associative_item(the_assoc_list, the_key)
	(*
		Get the value for the given key in an associative list.

		the_assoc_list : associative list
		the_key : anything -- the key to search for
		Result : anything -- the value, if found

		Note: Raises error -1728 if the key isn't found.
	*)
	set record_ref to find_record_for_key(the_assoc_list, the_key)
	if record_ref = missing value then
		error "The key wasn't found." number -1728 from the_key
	end if
	return the_value of record_ref
end get_associative_item


on set_associative_item(the_assoc_list, the_key, the_value)
	(*
		Set the value for the given key in an associative list.

		the_assoc_list : associative list
		the_key : anything -- the key to use
		the_value : anything -- the new value
	*)
	set record_ref to find_record_for_key(the_assoc_list, the_key)
	if record_ref = missing value then
		set end of the_items of the_assoc_list to {the_key:the_key, the_value:the_value}
	else
		set the_value of record_ref to the_value
	end if
	return -- No return value; the handler modifies the existing associative list.
end set_associative_item


on count_associative_items(the_assoc_list)
	(*
		Return the number of items in an associative list.

		the_assoc_list : associative list
		Result : integer
	*)
	return count the_items of the_assoc_list
end count_associative_items


on delete_associative_item(the_assoc_list, the_key)
	(*
		Delete the value for the given key.

		the_assoc_list : associative list
		the_key : anything -- the key to delete
	*)
	set record_ref to find_record_for_key(the_assoc_list, the_key)
	if record_ref is missing value then
		error "The key wasn't found." number -1728 from the_key
	end if
	set contents of record_ref to missing value
	set the_items of the_assoc_list to every record of the_items of the_assoc_list
	return -- No return value; the handler modifies the existing associative list.
end delete_associative_item


set friends_ages to make_associative_list()
set_associative_item(friends_ages, "Bob", 35)
set_associative_item(friends_ages, "Jan", 38)
delete_associative_item(friends_ages, "Bob")



Hi.

It looks as if it should work, but for some reason, trying to change the ‘contents’ of the reference value returned by find_record_for_key() is erroring instead of changing the contents. It’s possible to get round this by rewriting the script as below so that the handler returns an index instead of a reference, but it doesn’t explain why the error occurrs in the first place. :confused:

Another interesting thing about the script is that, whereas the associative list is set as a record containing both a ‘class’ and a ‘the_items’ property, looking at the result in Script Debugger only shows the record with the ‘the_items’ property, while looking at it in Script Editor shows both properties and throws a -1700 error! Both anomalies appear to be connected with the use of the AppleScript ‘class’ keyword as a property label.



on make_associative_list()
	(* Make a new associative list that stores values using any objects as keys.

		Result: an associative list

		Note: Users should not directly manipulate the contents of the returned 
		object. Instead, use the handlers provided to work with it safely.
	*)
	return {class:"associative list", the_items:{}}
end make_associative_list


on find_record_for_key(the_assoc_list, the_key)
	(* This is a private handler. Users should not use it directly. *)
	set the_items to the_items of the_assoc_list
	considering diacriticals, hyphens, punctuation and white space but ignoring case
		repeat with item_idx from 1 to (count the_items)
			if the_key of item item_idx of the_items = the_key then return item_idx
		end repeat
	end considering
	return missing value -- The key wasn't found
end find_record_for_key


on get_associative_item(the_assoc_list, the_key)
	(*
		Get the value for the given key in an associative list.

		the_assoc_list : associative list
		the_key : anything -- the key to search for
		Result : anything -- the value, if found

		Note: Raises error -1728 if the key isn't found.
	*)
	set item_idx to find_record_for_key(the_assoc_list, the_key)
	if item_idx = missing value then
		error "The key wasn't found." number -1728 from the_key
	end if
	return the_value of item item_idx of the_items of the_assoc_list
end get_associative_item


on set_associative_item(the_assoc_list, the_key, the_value)
	(*
		Set the value for the given key in an associative list.

		the_assoc_list : associative list
		the_key : anything -- the key to use
		the_value : anything -- the new value
	*)
	set item_idx to find_record_for_key(the_assoc_list, the_key)
	if item_idx = missing value then
		set end of the_items of the_assoc_list to {the_key:the_key, the_value:the_value}
	else
		set the_value of item item_idx of the_items of the_assoc_list to the_value
	end if
	return -- No return value; the handler modifies the existing associative list.
end set_associative_item


on count_associative_items(the_assoc_list)
	(*
		Return the number of items in an associative list.

		the_assoc_list : associative list
		Result : integer
	*)
	return count the_items of the_assoc_list
end count_associative_items


on delete_associative_item(the_assoc_list, the_key)
	(*
		Delete the value for the given key.

		the_assoc_list : associative list
		the_key : anything -- the key to delete
	*)
	set item_idx to find_record_for_key(the_assoc_list, the_key)
	if item_idx is missing value then
		error "The key wasn't found." number -1728 from the_key
	end if
	set item item_idx of the_items of the_assoc_list to missing value
	set the_items of the_assoc_list to every record of the_items of the_assoc_list
	return -- No return value; the handler modifies the existing associative list.
end delete_associative_item


set friends_ages to make_associative_list()
set_associative_item(friends_ages, "Bob", 35)
set_associative_item(friends_ages, "Jan", 38)
delete_associative_item(friends_ages, "Bob")
return friends_ages --> {class:"associative list", the_items:{{the_key:"Jan", the_value:38}}} + error in SE, {the_items:{{the_key:"Jan", the_value:38}}} in SD.


Hello Nigel

It seems that the problem may be easily solved.

on make_associative_list()
   (* Make a new associative list that stores values using any objects as keys.

       Result: an associative list

       Note: Users should not directly manipulate the contents of the returned 
       object. Instead, use the handlers provided to work with it safely.
   *)
   return {|class|:"associative list", the_items:{}} # ADDED pipes enclosing class which is no longer treated as a reserved word
end make_associative_list

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) mercredi 12 juin 2019 21:24:36

Not so easily. Exists error in handler “delete”… I believe that error starts from “find” handler

Hi Yvan.

Adding pipes avoids the anomalies when displaying the record as a result in Script Editor and Script Debugger. But I suspect that Hamish and Hanaan deliberately used the AppleScript keyword in order to be able to do tests like get class of friends_ages.

The error Tony’s been getting is to do with the reference returned by the find_record_for_key() handler. It correctly refers to an item in the list, but trying to set its ‘contents’ causes an error for some reason. Theoretically it should work, and presumably it did when the book was written.

I was writing about the script revised by Nigel, not about the original version.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) jeudi 13 juin 2019 04:51:59

Thank you! Not to be overdramatic about it, but I thought I understood the relatively simple idea of “contents of” a reference, yet suddenly what I thought I had learned stopped making sense. That’s a relief.

It seems that in Mojave at least:
• It’s possible to get the ‘contents’ of a reference to an item in a list which is a property of a record or script object.
• It’s possible to use the reference to change values associated with the item.
• It’s not possible to replace the item itself using the ‘contents’ operator.
• However, if the reference is just to an item in a list, with no mention of a containing record or script object, it is possible to use the ‘contents’ operator to replace the item. So while this self-contained version of the deleting handler errors …

on delete_associative_item(the_assoc_list, the_key)
	(*
		Delete the value for the given key.

		the_assoc_list : associative list (which is actually a record!)
		the_key : anything -- the key to delete
	*)
	repeat with record_ref in the_items of the_assoc_list -- record_ref is a reference to an item in the list property of the record.
		if the_key of record_ref = the_key then -- Works.
			set contents of record_ref to missing value -- Errors.
			set the_items of the_assoc_list to every record of the_items of the_assoc_list
			return
		end if
	end repeat
	
	error "The key wasn't found." number -1728 from the_key
end delete_associative_item

… this version works:

on delete_associative_item(the_assoc_list, the_key)
	(*
		Delete the value for the given key.

		the_assoc_list : associative list (which is actually a record!)
		the_key : anything -- the key to delete
	*)
	set the_items to the_items of the_assoc_list -- Set a variable directly to the list.
	repeat with record_ref in the_items -- record_ref is now just a reference to an item in the list. 
		if the_key of record_ref = the_key then -- Works.
			set contents of record_ref to missing value -- Also works.
			set the_items of the_assoc_list to every record of the_items -- of the_assoc_list
			return
		end if
	end repeat
	
	error "The key wasn't found." number -1728 from the_key
end delete_associative_item

Yes. Beautiful. And I believe I understand it.

So when we create this handler’s the_items, any already-existing reference to any of the items in (the_items of the assoc_list) will also refer to that item as it exists in this handler’s the_items. Correct?

Yes. When you set a variable or, say, a record property or a list item to an existing list, it’s the same list object. You’re just creating another way to access it. But if you copy a list to a variable (or record property or list item), the variable (or whatever) is set to a duplicate of the list. Changing the contents of this doesn’t affect what’s in the original.

By the way, I’ve now had a chance to try the original script on my El Capitan and Leopard systems and the same error occurs there. So the restriction on setting the ‘contents’ of a reference has been around for a while. I’ve not heard of it before, so I imagine not many people have actually had cause to run into it.