Perl-ish map and grep functions for AppleScript

I’m more of a Perl hacker than AppleScripter, so I was wondering if it would be possible to implement two of my favorite Perl functions as AppleScript handlers – map() and grep(). The former takes a list and a block of code, runs that code for each item in the list, and then returns the resulting list. The latter takes the same arguments, but returns a list containing only those elements for which the code returns “true”.

I learned a bit about script objects in the process, and it’s nice to see that AppleScript has enough support for first-class functions to do this kind of higher-order programming. You can even do closures, though it would be nice if it were possible to define anonymous script objects or handlers instead of always having to name them. I’d love to hear any tips for accomplishing that.

Enough talk, here’s the code:


on map over theList given script:theScript
	set resultList to {}
	repeat with theItem in theList
		set resultList to resultList & theScript's lambda(theItem)
	end repeat
	return resultList
end map

on grep over theList given script:theScript
	set resultList to {}
	repeat with theItem in theList
		tell theScript
			if lambda(theItem) is true then
				set resultList to resultList & theItem
			end if
		end tell
	end repeat
	return resultList
end grep

-- end library

script mapScript
	property HowMany : 0
	
	on lambda(someone)
		set HowMany to HowMany + 1
		return someone & " Gardner " & HowMany
	end lambda
end script
map over {"Mark", "Erin", "David"} given script:mapScript

script grepScript
	on lambda(someone)
		considering case
			if contents of someone is equal to "David" then
				return true
			end if
			return false
		end considering
	end lambda
end script
grep over {"Mark", "Erin", "David"} given script:grepScript

Model: MacBook Pro
AppleScript: 2.0.1
Browser: Safari 531.21.10
Operating System: Mac OS X (10.5)

Anonymous Script Objects
Technically, you can have anonymous script objects, though they can not be “inline” since script is a statement not an expression. So they are still not as nice as Perl’s blocks-as-coderefs or even its anonymous subs.

script
   on lambda(someone)
-- .
   end lambda
end script
grep over {"Mark", "Erin", "David"} given script:result

Accordingly,

script foo
.
end

is almost exactly the same as

script
.
end
set foo to result

List Accumulation
For list accumulation, the common AppleScript idiom goes like this:

set newList to {}
.
set end of newList to aValue

This directly appends to the list instead of having to create a whole new copy as you are doing with the list concatenation operator.

For performance reasons, sometimes this is written in a slight obtuse manner:


script helper
property newList : {}
end
.
set end of helper's newList to aValue

They do the same thing, but the second version is more efficient due to the way AppleScript is implemented (see an AppleScript-users email by Nigel Garvey on the “Serge method”).

Limiting the Scope of Tell Blocks
In your grep handler, you have more code in the tell block than is strictly necessary. Actually, as in your map handler, the tell block is not needed at all. The is true is only useful for avoiding a run-time error in the case where lambda returns a non-boolean value. It seems to me that it is likely to be a bug to pass such a handler to your grep, so I would skip is true and let the run-time error happen.

Implicit References with “repeat with X in” Loops
As you may have noticed, using repeat with X in someListValue causes X to be a reference to the item in someListValue instead of the value itself. You can see the difference in the following example:

to noderef()
	repeat with x in {"A"}
		return x
	end repeat
end noderef
to deref()
	repeat with x in {"A"}
		return contents of x
	end repeat
end deref

noderef() --> item 1 of {"A", "B", "C"}
deref() --> "A"

Both of your handlers pass the reference, which is true to the Perl semantics, but might be a bit surprising to some AppleScript users. You might consider abandoning the Perl semantics and passing the dereferenced value, or embracing the Perl semantics and documenting the reference (which unfortunately is not quite as transparent as the way Perl does it).

Using map to Remove Items or Add Extra Items
In Perl map sub/block you can return zero, one, or more values. If you want to more fully embrace the Perl semantics, you could do the same thing. The gotcha here is that to map to a list of exactly one item you have to wrap the value in another list. This does not come up in Perl because returning a list is different from returning an “arrayref” (return @somelist vs. return [@somelist]).

Here is a more Perl-y version with some of the other changes I have mentioned:

on map over theList given script:theScript
	set resultList to {}
	repeat with theItem in theList
		set newValue to theScript's lambda(theItem)
		if class of newValue is list then
			if length of newValue is 1 then
				-- can directly append because we only have one item to append
				set end of resultList to first item of newValue
			else if length of newValue is greater than 1 then
				-- must concatenate because we are appending more than one item
				set resultList to resultList & newValue
			end if
		else
			-- can directly append because we only have one item to append
			set end of resultList to newValue
		end if
	end repeat
	return resultList
end map

on grep over theList given script:theScript
	set resultList to {}
	repeat with theItem in theList
		if theScript's lambda(theItem) then
			set end of resultList to contents of theItem
		end if
	end repeat
	return resultList
end grep

-- end library

script mapScript
	property HowMany : 0
	
	on lambda(someone)
		set HowMany to HowMany + 1
		set newValue to someone & " Gardner " & HowMany
		if HowMany is equal to 1 then
			-- map to a list of one item
			set newValue to {{newValue}}
		else if HowMany is equal to 2 then
			-- map to nothing
			set newValue to {}
		else if HowMany is equal to 3 then
			-- modify original list and
			-- map to multiple values
			set contents of someone to "frobbed!"
			set newValue to {"-->", newValue, "<--"}
		end if
		newValue
	end lambda
end script
set aList to {"Mark", "Erin", "David"}
map over aList given script:mapScript -->{{"Mark Gardner 1"}, "-->", "David Gardner 3", "<--"}
aList --> {"Mark", "Erin", "frobbed!"}

script grepScript
	on lambda(someone)
		set val to contents of someone
		considering case
			if val is equal to "David" then
				return true
			end if
			if val is equal to "Mark" then
				-- modify original list
				set contents of someone to "also frobbed!"
			end if
			return false
		end considering
	end lambda
end script
set aList to {"Mark", "Erin", "David"}
grep over aList given script:grepScript --> {"David"}
aList --> {"also frobbed!", "Erin", "David"}

Passing a Handler Without a Script Object
It is possible to pass a handler directly, but it might not do what you want:

property foo : "global"

to doSomething(x)
	"something (" & x & "; " & foo & ")"
end doSomething

to doSomethingBy by x
	"something else (" & x & "; " & foo & ")"
end doSomethingBy

to useIt(aHandler)
	script wrapper
		property foo : "useIt"
		property theHandler : missing value
	end script
	set wrapper's theHandler to aHandler
	wrapper's theHandler("x")
end useIt

to useItBy(aHandler)
	script wrapper
		property foo : "useItBy"
		property theHandler : missing value
	end script
	set wrapper's theHandler to aHandler
	wrapper's (theHandler by "x")
end useItBy

global aGlobalHandler
to gUseIt(aHandler)
	set aGlobalHandler to aHandler
	aGlobalHandler("g")
end gUseIt
to gUseItBy(aHandler)
	set aGlobalHandler to aHandler
	aGlobalHandler by "g"
end gUseItBy

script bla
	property foo : "bla"
	to blah(x)
		"blah (" & x & "; " & foo & ")"
	end blah
	to blahBy by x
		"blah by (" & x & "; " & foo & ")"
	end blahBy
end script

useIt(doSomething) --> "something (x; useIt)"
gUseIt(doSomething) --> "something (g; global)"
useItBy(doSomethingBy) --> "something else (x; useItBy)"
gUseItBy(doSomethingBy) --> "something else (g; global)"

bla's blah("b") --> "blah (b; bla)"
blahBy of bla by "b" --> "blah by (b; bla)"

useIt(bla's blah) --> "blah (x; useIt)"
gUseIt(bla's blah) --> "blah (g; global)"
useItBy(bla's blahBy) --> "blah by (x; useItBy)"
gUseItBy(bla's blahBy) --> "blah by (g; global)"

I have found passing script objects to be much cleaner and less error prone (e.g. I get a very misleading error message if the wrapper scripts do not include their own foo property: “Can’t make «handler doSomething» into type string.” with foo in doSomething highlighted).

Model: iBook G4 933
AppleScript: 1.10.7
Browser: Safari 4.0.4 (4531.21.10, r51280)
Operating System: Mac OS X (10.4)