# Using concatMap with closures in AppleScript

@alldritt’s thoughts in a previous thread:

Using map, filter and reduce or fold in Applescript

reminded me that:

1. If I were to add a fourth generic function to the basic map filter fold toolkit, it would be concatMap, and that
2. I hadn’t mentioned the capture of closure values in the pattern of passing script objects to these functions.

concatMap is almost identical to map, but it additionally concatenates the list of values produced by mapping.

This allows it to provide a kind of filtering. If a handler or script that is passed to it

• wraps each return value in a list, and
• for some values returns an empty list,

then concatenation eliminates those empty values.

The simplest use would be as a combination of map and filter - both transforming and occasionally pruning out each element in a list.

If we nest a few concatMaps, passing scripts to them (rather than handlers), and capturing closure values into those scripts, we can use this (transform + perhaps eliminate) pattern to write an AppleScript version of a list comprehension.

For example, how many Pythagorean triples can we find using only the integers from 1 to 25 ?

``````-- pythagoreanTriples :: Int -> [(Int, Int, Int)]
on pythagoreanTriples(n)
script x
on |λ|(x)
script y
on |λ|(y)
script z
on |λ|(z)
if x * x + y * y = z * z then
{{x, y, z}}
else
{}
end if
end |λ|
end script

concatMap(z, enumFromTo(1 + y, n))
end |λ|
end script

concatMap(y, enumFromTo(1 + x, n))
end |λ|
end script

concatMap(x, enumFromTo(1, n))
end pythagoreanTriples

-- TEST -----------------------------------------------------------------------
on run
--   Pythagorean triples drawn from integers in the range [1..n]
--  {(x, y, z) | x <- [1..n], y <- [x+1..n], z <- [y+1..n], (x^2 + y^2 = z^2)}

pythagoreanTriples(25)

--> {{3, 4, 5}, {5, 12, 13}, {6, 8, 10}, {7, 24, 25}, {8, 15, 17},
--   {9, 12, 15}, {12, 16, 20}, {15, 20, 25}}
end run

-- GENERIC FUNCTIONS ----------------------------------------------------------

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
set acc to {}
tell mReturn(f)
repeat with x in xs
set acc to acc & |λ|(contents of x)
end repeat
end tell
return acc
end concatMap

-- enumFromTo :: Int -> Int -> [Int]
on enumFromTo(m, n)
if n < m then
set d to -1
else
set d to 1
end if
set lst to {}
repeat with i from m to n by d
set end of lst to i
end repeat
return lst
end enumFromTo

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: Handler -> Script
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn
``````

We can also combine concatMap with the capture of closure values to define a generic cartesianProduct handler.

First a trivial example, and then a more useful one:

Simple example

``````on run
cartesianProduct({"Important", "Unimportant"}, {"Urgent", "Not urgent"})

--> {{"Important", "Urgent"}, {"Important", "Not urgent"}, {"Unimportant", "Urgent"}, {"Unimportant", "Not urgent"}}

cartesianProduct({"Alpha", "Beta", "Gamma"}, {1, 2, 3, 4})

--> {{"Alpha", 1}, {"Alpha", 2}, {"Alpha", 3}, {"Alpha", 4}, {"Beta", 1}, {"Beta", 2}, {"Beta", 3}, {"Beta", 4}, {"Gamma", 1}, {"Gamma", 2}, {"Gamma", 3}, {"Gamma", 4}}
end run

-- cartesianProduct :: [a] -> [b] -> [(a, b)]
on cartesianProduct(xs, ys)
script
on |λ|(x)
script
on |λ|(y)
{{x, y}}
end |λ|
end script
concatMap(result, ys)
end |λ|
end script
concatMap(result, xs)
end cartesianProduct

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
set lng to length of xs
set acc to {}
tell mReturn(f)
repeat with i from 1 to lng
set acc to acc & |λ|(item i of xs, i, xs)
end repeat
end tell
return acc
end concatMap

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn
``````

More useful example (looking at the grid of (clipboard types) * (alternative representations) in the current clipboard)

``````use framework "Foundation"
use framework "Appkit"

-- INSPECTING DIFFERENT TYPES OF DATA ON THE CLIPBOARD

-- showClipBoard :: () -> Record
on showClipboard()
set ca to current application
set pBoard to ca's NSPasteboard's generalPasteboard
set clipTypes to (item 1 of (pBoard's pasteboardItems as list))'s types as list
set types to {"propertyList", "string", "data"}

script dataFor
on |λ|(accumulator, bundleType)
set {bundleID, k} to bundleType

if k = "string" then
set v to pBoard's stringForType:bundleID
else if k = "propertyList" then
set v to pBoard's propertyListForType:bundleID
else -- "data"
set v to ca's NSString's alloc()'s ¬
initWithData:(pBoard's dataForType:bundleID) ¬
encoding:(ca's NSUTF8StringEncoding)
end if
if v is missing value then
accumulator
else
recordInsert(accumulator, bundleID & " as " & k, v)
end if
end |λ|
end script

foldl(dataFor, {name:""}, cartesianProduct(clipTypes, types))
end showClipboard

-- TEST ------------------------------------------------------------------
on run

showClipboard()

end run

-- GENERIC FUNCTIONS -----------------------------------------------------

-- cartesianProduct :: [a] -> [b] -> [(a, b)]
on cartesianProduct(xs, ys)
script
on |λ|(x)
script
on |λ|(y)
{{x, y}}
end |λ|
end script
concatMap(result, ys)
end |λ|
end script
concatMap(result, xs)
end cartesianProduct

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
set acc to {}
tell mReturn(f)
repeat with x in xs
set acc to acc & |λ|(contents of x)
end repeat
end tell
return acc
end concatMap

-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
tell mReturn(f)
set v to startValue
set lng to length of xs
repeat with i from 1 to lng
set v to |λ|(v, item i of xs, i, xs)
end repeat
return v
end tell
end foldl

-- recordInsert :: Dict -> k -> v -> Dict
on recordInsert(rec, k, v)
set ca to current application
set nsDct to (ca's NSMutableDictionary's dictionaryWithDictionary:rec)
nsDct's setValue:v forKey:(k as string)
item 1 of ((ca's NSArray's arrayWithObject:nsDct) as list)
end recordInsert

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn
``````

and, of course, the capture of closure values can be useful with any of these functions.

Again, a trivial example, this time with map:

``````use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"

on run

set fp to filePath("~/Desktop")

script fullPath
on |λ|(x)
fp & "/" & x & ".txt"
end |λ|
end script

map(fullPath, {"alpha", "beta", "gamma"})

end run

-- filePath :: String -> FilePath
on filePath(s)
(stringByStandardizingPath of ¬
(current application's ¬
NSString's stringWithString:(s))) as string
end filePath

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
tell mReturn(f)
set lng to length of xs
set lst to {}
repeat with i from 1 to lng
set end of lst to |λ|(item i of xs, i, xs)
end repeat
return lst
end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn

``````

Or for a more useful example of capturing closure values with map, a transpose function for flipping rows ⇄ columns.

(This basic version assumes that the rows are all of equal length)

``````-- TRANSPOSE -----------------------------------------------------------------

-- transpose :: [[a]] -> [[a]]
on transpose(rows)
script column
on |λ|(_, iCol)
script row
on |λ|(xs)
item iCol of xs
end |λ|
end script

map(row, rows)
end |λ|
end script

map(column, item 1 of rows)
end transpose

-- TEST ----------------------------------------------------------------------
on run
transpose([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])

--> {{1, 4, 7, 10}, {2, 5, 8, 11}, {3, 6, 9, 12}}
end run

-- GENERIC FUNCTIONS ---------------------------------------------------------

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
tell mReturn(f)
set lng to length of xs
set lst to {}
repeat with i from 1 to lng
set end of lst to |λ|(item i of xs, i, xs)
end repeat
return lst
end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: Handler -> Script
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn
``````
1 Like

Two slightly off-topic points:

• There was no Matt in that thread; I suspect you mean @alldritt .

• Although many Objective-C properties (and methods that take no parameters) can usually be called in ASObjC without the trailing parentheses, doing so invokes different behavior. When you leave off the parentheses, you are essentially using key-value coding, similar to invoking `valueForKey:`. That means the bridging between AppleScript and Objective-C is done without recourse to the method or property signatures. In many cases the result is identical, but in others it’s not — sometimes subtly, and sometimes less so. And there have been slight differences between OS versions. You may have been aware of all this, and do it only in cases where you know there will be no difference. But if you’re adopting it as a general preference just because it seems to work, I suggest you reconsider.

(Given AppleScript’s inability to discern Cocoa properties from variables when applying syntax styling, I’d also argue that it potentially detracts from readability in Script Debugger, where separate syntax coloring is available for handler names. But that’s verging on a matter of taste.)

1 Like

When you leave off the parentheses, you are essentially using key-value coding, similar to invoking valueForKey

Matt -> Mark

thanks again ! – I’ll fix that above.

Thanks for all this. I’ll need to come back to it when the neighbours are out (banging my head on the wall tends to upset them), but I can see the value of the principle. Stripping out the empty values is something I’d probably have done with the result (although sometimes the lack of a value is itself useful info), so this looks like a useful time/code saver.

2 Likes

In case you want anything more to experiment with – another use of concatMap – flattening nested lists.

For those moments when you end up with an arbitrarily nested list, and just want to obtain a flattened version of it:

``````-- flatten :: NestedList a -> [a]
on flatten(t)
if list is class of t then
concatMap(my flatten, t)
else
t
end if
end flatten

-- TEST ----------------------------------------------------------------
on run

flatten({"alpha", {{{"beta"}}, {"gamma", "delta"}}, "epsilon", ¬
{"zeta", "eta", "theta"}, {{"iota", "kappa"}}, "lambda", "mu"})

--> {"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"}
end run

-- GENERIC -------------------------------------------------------------

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
set lng to length of xs
set acc to {}
tell mReturn(f)
repeat with i from 1 to lng
set acc to acc & |λ|(item i of xs, i, xs)
end repeat
end tell
return acc
end concatMap

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn
``````

Just noticed one river that flows into that habit – the `Automation.ObjC` interface of JS for Automation pushes in the opposite direction – offering only a `not a function` error in exchange for parentheses appended to ObjC method names. Thank you for alerting me to the AS pattern.

So what’s returned when you call, say, `rangeOfString:` in JXA?

or:

``````use framework "Foundation"
use framework "AppKit"

set aNum to current application's NSScreen's mainScreen()'s frame()
``````

Interesting, thanks. So is there a way to get an NSValue like this:

``````use framework "Foundation"
use framework "AppKit"

set aNum to current application's NSScreen's mainScreen()'s frame
``````

or an array of them like this:

``````use framework "Foundation"
use framework "AppKit"

current application's NSScreen's screens()'s valueForKey:"frame"
``````

?

You can write these kind of things:

From the release notes:

The \$() operator is a convenient alias for ObjC.wrap(), and is meant to be similar to the @() boxing operator in ObjC. For example, \$(“foo”) returns a wrapped NSString instance, and \$(1) returns a wrapped NSNumber instance.