Declaring Cocoa classes as properties not merely a convenience but a necessity

This one gave me a bit of a hard time.
It never occured to me that to solve the puzzle I had to declare a Cocoa class as a property. And I also never read about it. Up to now, I have always considered this a mere refactoring convenience to save a lot of unnecessary typing, offered by Script Debugger under Edit > AppleScriptObjC > Migrate to Properties.

My case:

my sortList({"B", "A"})

on sortList(theList)
	script o
		use framework "Foundation"
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

The above works.

use framework "Foundation"

my sortList({"B", "A"})

on sortList(theList)
	script o
		--use framework "Foundation"
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

This one doesn’t.

use framework "Foundation"

my sortList({"B", "A"})

on sortList(theList)
	script o
		use framework "Foundation"
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

The above code also does not work.

So what to do if I intend to use ASObjC also in the main, outer script?
Tried this and that to no avail. The solution came by chance: I had to declare a property referencing NSArray in the main script to make it work. Not difficult to comprehend in hindsight. But even SD’s documentation gives no hint relating to this kind of substantial difference of this kind of “code refactoring”. At least I couldn’t find any.

use framework "Foundation"

-- classes, constants, and enums used
property NSArray : a reference to current application's NSArray

my sortList({"B", "A"})

on sortList(theList)
	script o
		on AsObjCsort(theList)
			set theArray to NSArray's arrayWithArray:theList
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

Just to spare somebody from pulling his or her hair over this.

On a side note:
I have seen scripts fail sometimes (= inconsistently) when calling script libraries containing ASObjC code if the calling script did not declare the same frameworks as the ones used in the library.

Anybody with similarly odd observations?

When the compiler encounters a use framework "Foundation" or other use framework "..." statement during compile time, the compiler loads the framework and makes its Cocoa classes available to the script, effectively turning an AppleScript script into an ASObjC script. When the use framework "..." statement is encountered later during runtime and not during the initial script compilation, which is the case when it is present in a script object embedded within a handler, that loading and exposure do not take place, and Cocoa classes are not recognized when the embedded script object is executed. A number of years ago, Shane Stanley posted an elegant but little known technique that tricks the compiler into “seeing” the embedded use framework "..." statement during the initial compilation. The technique is to include the following statement in the embedded script:

property parent : current application

I have been using it for many years, and it has worked flawlessly. Here is its implementation in your original script:

my sortList({"B", "A"}) --> {"A", "B"}

on sortList(theList)
	script o
		use framework "Foundation"
		property parent : current application
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

A use framework "Foundation" or other use framework "..." statement may appear in the top-level script as well, if needed. If a use scripting additions statement is needed (as is usually the case), it can appear in the script only once and usually should be placed in the top-level script. These additions are shown in the following version of your script:

use framework "Foundation"
use scripting additions -- this must appear only once, usually in the top-level script

my sortList({"B", "A"}) --> {"A", "B"}

-- ... ASObjC statments may now be used in the top-level script ... --

on sortList(theList)
	script o
		use framework "Foundation"
		property parent : current application
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

@bmose: Thanks for your thorough explanation. It’s not really a surprise that this works, if I get it explained like that, but once again most likely I wouldn’t have found the solution on my own. ASObjC is a kludge, albeit an indispensable one (now that osaxen are gone).
Furthermore, I googled and found nothing - either nothing that hit the nail or nothing at all. I also searched Shane Stanley’s invaluable PDF-books on ASObjC without a match - except the discussion about “open for access” in ASObjC which can be resolved by a tell-block to current application (cf. AppleScriptObjC Explored 5, p. 212.)

Regarding my sidenote: this may mean that it may be possible to avoid script crashes by adding the line property parent : current application in the library file? I haven’t tried it yet.

Again, thanks for sharing your insight!

Glad it helped. I struggled with the same problem until I stumbled on Shane’s sample script (whose purpose, if I recall correctly, did not have to do with the property parent : current application statement!)

To elaborate a bit, the property parent : current application statement is not needed in two circumstances: (1) the script as a whole, and (2) a top-level script object within the script as a whole. But it should/must be used in a script object embedded within a handler or within another script object, as demonstrated in the following scripts:

use framework "Cocoa"
((current application's NSString's stringWithString:"script as a whole; 'property parent...' not required")'s uppercaseString()) as text
--> "SCRIPT AS A WHOLE; 'PROPERTY PARENT...' NOT REQUIRED"
script s1
	use framework "Cocoa"
	return ((current application's NSString's stringWithString:"top-level script object; 'property parent...' not required")'s uppercaseString()) as text
end script

run s1
--> "TOP-LEVEL SCRIPT OBJECT; 'PROPERTY PARENT...' NOT REQUIRED"
script s1
	script s2
		use framework "Cocoa"
		property parent : current application
		return ((current application's NSString's stringWithString:"script object embedded in a script object; 'property parent...' required")'s uppercaseString()) as text
	end script
end script

run s1's s2
--> "SCRIPT OBJECT EMBEDDED IN A SCRIPT OBJECT; 'PROPERTY PARENT...' REQUIRED"
on h1()
	script s1
		use framework "Cocoa"
		property parent : current application
		return ((current application's NSString's stringWithString:"script object embedded in a handler; 'property parent...' required")'s uppercaseString()) as text
	end script
end h1

run h1()'s s1
--> "SCRIPT OBJECT EMBEDDED IN A HANDLER; 'PROPERTY PARENT...' REQUIRED"

OK - thanks again.
I admit this is quite a bit counterintuitive.
I tried to move the property parent as well as the framework declaration to various levels of the script, embedding script and main script but, naturally to no avail. If I return to my original scenario “ASObjC in the main script also” it turns out that I need an additional framework declaration in the main script, like this:

use framework "Cocoa"

script s1
	script s2
		use framework "Cocoa"
		property parent : current application
		return ((current application's NSString's stringWithString:"script object embedded in a script object; 'property parent...' required")'s uppercaseString()) as text
	end script
end script

log (run s1's s2)
log ((NSString's stringWithString:"Howdy")'s uppercaseString()) as text

So the bottom line seems to be:
Use “property parent : current application” in an embedded script object to (as you said) trick the system into realizing that the framework is needed in the embedded script.

In the end, however, I feel that my accidentally discovered “refactoring method” seems to be easier, i. e. less complicated and verbose. I just declare the framework and references to the ObjC-classes at the top level and I’m done - like this:

use framework "Cocoa"

-- classes, constants, and enums used
property NSString : a reference to current application's NSString

script s1
	script s2
		--use framework "Cocoa"
		--property parent : current application
		return ((NSString's stringWithString:"script object embedded in a script object; 'property parent...' required")'s uppercaseString()) as text
	end script
end script

log (run s1's s2)
log ((NSString's stringWithString:"Howdy")'s uppercaseString()) as text

And this seems, wonder of wonders, to work in all the usage cases described by you, see the following example:

use framework "Cocoa"

-- classes, constants, and enums used
property NSString : a reference to current application's NSString

script s1
	((NSString's stringWithString:"script1")'s uppercaseString()) as text
	script s2
		--use framework "Cocoa"
		--property parent : current application
		return ((NSString's stringWithString:"script2")'s uppercaseString()) as text
	end script
end script

log (run s1)
log (run s1's s2)
log ((NSString's stringWithString:"main script")'s uppercaseString()) as text

Or am I overlooking something?
Thanks again!

Your approach works because:

  • Your script-as-a-whole has Cocoa framework access via its use framework "Cocoa" statement (which, because it is at the top level, doesn’t require an accompanying property parent : current application statement), and
  • Your property NSString... statement stores a reference to the NSString class of that framework, and
  • That class becomes available to any child object of the script-as-a-whole by AppleScript’s standard rules of reference resolution. Script s2 can’t resolve NSString at its own level. So it moves to the next higher level in the hierarchy chain, script s1. It can’t resolve NSString in script s1. So it moves to the next higher level in the hierarchy cahin, the script as a whole, where it is able to resolve the reference

Your approach is a perfectly acceptable: one use framework "..." statement + separate property statements for each Cocoa class in the script-as-a-whole, which are now available to all embedded script objects. A potential downside, depending on one’s perspective, might be the case where an embedded script object (such as script s2 in the current example) needs access to a multitude of Cocoa classes, in which case you would have to encode references to each of those classes in separate property statements in the script-as-a-whole. If you later edit your script by adding new Cocoa class references or deleting old class references that are no longer needed, you would have to remember to encode a separate property statement for each new class, and ideally delete references to classes that are no longer used.

I prefer the simplicity of one use framework "..." and an accompanying property parent : current application statement in all embedded script objects, which gives the embedded script objects access to all Cocoa classes of that framework. It amounts to repetitive but brainless housework, as opposed to the individualized housework with your approach. Of course, as you pointed out, with my approach, each script object that requires access to Cocoa classes would need its own use framework "..." statement, whether it is the script-as-a-whole, a top level script object, or an embedded script object, and embedded script objects would also need an accompanying property parent : current application statement.

It comes down to one’s preferred coding style.

OK, that’s quite interesting. Thanks for this further elucidation. There are obviously some subtle distinctions to be aware of - which I was not, of course.
So the bottom line may be to choose one’s approach according to what fits best in the respective circumstances, instead of a one size fits it all approach. And your approach seems to have the advantage of creating somewhat more self contained code: everything is exactly where it belongs to be, like here:

use framework "Cocoa"

script s1
	use framework "Cocoa"
	property parent : current application
	((current application's NSString's stringWithString:"script1")'s uppercaseString()) as text
	script s2
		use framework "Cocoa"
		property parent : current application
		return ((current application's NSString's stringWithString:"script2")'s uppercaseString()) as text
	end script
end script

log (run s1)
log (run s1's s2)
log ((current application's NSString's stringWithString:"main script")'s uppercaseString()) as text

In this case one would be able to replace s1 or s2 by something completely different without having to bother about the cross dependencies to the main script in my approach. That is certainly a valid argument!

Excellent, once again: thanks a lot for sharing your insights!

pjh, you may have brought to light some of AppleScriptObjC’s dirty underwear! At this moment, during this login, on my computer, running this OS (Tahoe), the following works:

my sortList({"B", "A"}) --> {"A", "B"}

on sortList(theList)
	script o
		use framework "Cocoa"
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

but the following fails:

use framework "Foundation"

my sortList({"B", "A"}) --> error: 'NSArray doesn’t understand the “arrayWithArray_” message.

on sortList(theList)
	script o
		use framework "Foundation"
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

Incredibly, it is the presence of a use framework "..." statement in the script-as-a-whole that prevents the embedded script object that is instantiated during runtime from making the connection to the ASObjC bridge! When the script-as-a-whole is plain AppleScript, that error does not occur, but when the script-as-a-whole is a bridged ASObjC script, the error occurs. From my vantage point, that is inconsistent, unexpected, non-obvious, and undocumented behavior that is worthy of submission to Apple’s Feedback Assistant as a bug, which I intend to do.

While we’re holding our breath waiting for Apple to fix this problem, either of the above two approaches we outlined above should be used.

Again, this is indeed a very interesting observation.
From my perspective, never having used a “Cocoa” declaration but always “Foundation” and other particular frameworks as needed, it wouldn’t have occured to me to try it as a possible alternative approach. So thank you for bringing it into the game.
This, of course, must be a bug, obviously. And yes, your second, failing example, was my point of departure and cause for pulling my hair. I simply couldn’t get what was happening.

Would you care to elaborate on when to use “Cocoa” specifically?
AFAIK, in the context of our examples “Cocoa” should be overkill, since it comprises “Foundation” + the GUI classes (which are not needed here).
Does using “Cocoa” instead of foundation have some kind of trade-off, e. g. memory consumption or else? The rule of thumb seems to be to use only the frameworks your code will be actually using.

Thanks again and thanks for reporting the bug! (Although I won’t hold my breath…)

It seems that one can come up with several kinds of variations to my three level “Cocoa” example replacing the latter by “Foundation”:
Works:

use framework "Foundation"

script s1
	--use framework "Foundation"
	--property parent : current application
	((current application's NSString's stringWithString:"script1")'s uppercaseString()) as text
	script s2
		use framework "Foundation"
		property parent : current application
		return ((current application's NSString's stringWithString:"script2")'s uppercaseString()) as text
	end script
end script

log (run s1)
log (run s1's s2)
log ((current application's NSString's stringWithString:"main script")'s uppercaseString()) as text

Does not work - seems that the innermost script object must contain the necessary declarations:

use framework "Foundation"

script s1
	use framework "Foundation"
	property parent : current application
	((current application's NSString's stringWithString:"script1")'s uppercaseString()) as text
	script s2
		--use framework "Foundation"
		--property parent : current application
		return ((current application's NSString's stringWithString:"script2")'s uppercaseString()) as text
	end script
end script

log (run s1)
log (run s1's s2)
log ((current application's NSString's stringWithString:"main script")'s uppercaseString()) as text

And replacing your second, failing examples “Foundation” statements by “Cocoa” makes it fail, too:

use framework "Cocoa"

my sortList({"B", "A"}) --> error: 'NSArray doesn’t understand the “arrayWithArray_” message.

on sortList(theList)
	script o
		use framework "Cocoa"
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

Only removing the outer script’s “Cocoa” declaration makes it work:

--use framework "Cocoa"

my sortList({"B", "A"}) --> error: 'NSArray doesn’t understand the “arrayWithArray_” message.

on sortList(theList)
	script o
		use framework "Cocoa"
		on AsObjCsort(theList)
			set theArray to (current application's NSArray's arrayWithArray:theList)
			set theArray to theArray's sortedArrayUsingSelector:"localizedStandardCompare:"
			return theArray as list
		end AsObjCsort
	end script
	return o's AsObjCsort(theList)
end sortList

So same story for “Cocoa” and “Foundation” - or am I mistaken?
And this one was driving me nuts.

From a purist viewpoint, the Foundation framework should be used for Foundation ASObjC work, and AppKit for AppKit work (NSWorkspace, NSImage, NSAlert, etc.) However, in the strange world of AppleScript, I’ve found that both (along with some other frameworks) get mysteriously loaded behind the scenes, and AppKit classes have universally worked for me even when use framework "Foundation" alone is specified. So, I’ve gotten in the habit of using use framework "Cocoa" to pretend that I’m explicitly loading both Foundation and AppKit rather than relying on under-the-hood magic to load AppKit (Cocoa = Foundation + AppKit.) Then, when I use some of the more common AppKit classes like NSWorkspace, I pretend that I’m not surprised that it works.

Oh, and yes, same story for “Cocoa” and “Foundation”.

:joy::joy::joy: OK, understood (and here the many :joy:s are simply good to reach the minimum character count of 20 to be able to post…)

We are probably reaching our limit of back-and-forths on this thread, but here goes…

I suppose it’s possible that the compiler doesn’t load the AppKit framework if it does not detect any AppKit classes in the script and only a use framework "Foundation" statement is present. So if you prefer the purist style and know that you are using only Foundation classes, it’s probably better to go with use framework "Foundation". I don’t know what happens under the hood, and I’ve chosen use framework "Cocoa" as the brainless and easy path at the risk of possible inefficiency. :joy: