Sunday, November 27, 2022

#1 2009-11-22 05:41:07 pm

mjg
Member
Registered: 2009-11-22
Posts: 1

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:

Applescript:


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)


Filed under: grep, perl, map, lambda

Offline

 

#2 2009-11-23 12:59:52 am

chrys
Member
From:: McKinney, TX, USA
Registered: 2007-06-26
Posts: 442

Re: Perl-ish map and grep functions for AppleScript

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.

Applescript:

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

Accordingly,

Applescript:

script foo
…
end

is almost exactly the same as

Applescript:

script
…
end
set foo to result

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

Applescript:

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:

Applescript:


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:

Applescript:

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:

Applescript:

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:

Applescript:

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)


--
Chris

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)