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"
use scripting additions


-- 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"
use scripting additions


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

That’s very helpful – thanks.

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()

vs. an error if frame -> frame()

Parentheses are treated as well-formed where arguments are expected:

Without them, in those cases, we just get a reference to a function:

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.