Every item of a list whose


(Ed Stockly) #1

I know that applescript’s whose clause has never worked on lists, but is there an ASObjC way to do the same thing?

set timesThisMovieisShowing to every item of a list of lists where item 2 is “The Great Escape”

or

set listOfFourStarFilms to every item of a list of lists where item 4 is “4”

or
set listOfBoxOfficeHits to every item of a list of lists where item 2 is in aListOfHitMovieTitles

In updating and old script I’m sorting these lists (much faster now with ASObjC) then stepping through them looking for matches.

I’m guessing there’s a faster way.


(Shane Stanley) #2

Yes, there’s an equivalent of whose clauses: NSPredicates. But the bad news is that they work based on properties of the items in the target array, but not on properties of elements of subarrays.

If you had a list of records, it would do-able.


(Ed Stockly) #3

Are there drawbacks to using records rather than lists? Would sorting a list of records work the same way?


(Shane Stanley) #4

Not really, other than perhaps generating them in the first place.

It’s actually easier.


(Ed Stockly) #5

One day, not that long ago a very wise man once told me that Records were clumsy and slow and should be avoided.


(Shane Stanley) #6

He was very wise, in a simple AppleScript context. But if you’re building them to manipulate via AppleScriptObjC, they become the very useful things they always should have been.

:wink:


#7

Without the ObjC interface they really are clumsy in AS:

  • they lack introspection – throw up their hands and shrug if you ask them what keys they’ve got, and
  • lack composure – ask them for the value of a key they don’t have, and they just panic – interrupting the whole computation with an error.

With the ObjC interface, however, they are calmer and more cooperative. For example:

-- keys :: Dict -> [String]
on keys(rec)
    (current application's NSDictionary's dictionaryWithDictionary:rec)'s allKeys() as list
end keys

#8

I use a mapFromList function (assembled from pre-fab parts), to do this:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    set xs to {"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", ¬
        "theta", "iota", "kappa", "lambda", "mu"}
    
    mapFromList(zip(xs, enumFromTo(1, length of xs)))
    
    --> {eta:7, mu:12, gamma:3, epsilon:5, beta:2, theta:8, kappa:10, zeta:6, alpha:1, iota:9, delta:4, lambda:11}
end run


-- mapFromList :: [(k, v)] -> Dict
on mapFromList(kvs)
    set tpl to unzip(kvs)
    script
        on |λ|(x)
            x as string
        end |λ|
    end script
    (current application's NSDictionary's ¬
        dictionaryWithObjects:(|2| of tpl) ¬
            forKeys:map(result, |1| of tpl)) as record
end mapFromList


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

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

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

-- min :: Ord a => a -> a -> a
on min(x, y)
    if y < x then
        y
    else
        x
    end if
end min

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

-- Tuple (,) :: a -> b -> (a, b)
on Tuple(a, b)
    {type:"Tuple", |1|:a, |2|:b, length:2}
end Tuple

-- unzip :: [(a,b)] -> ([a],[b])
on unzip(xys)
    set xs to {}
    set ys to {}
    repeat with xy in xys
        set end of xs to |1| of xy
        set end of ys to |2| of xy
    end repeat
    return Tuple(xs, ys)
end unzip

-- zip :: [a] -> [b] -> [(a, b)]
on zip(xs, ys)
    set lng to min(length of xs, length of ys)
    set lst to {}
    repeat with i from 1 to lng
        set end of lst to Tuple(item i of xs, item i of ys)
    end repeat
    return lst
end zip

(Shane Stanley) #9

My BridgePlus library also has methods for converting back and forth between lists of lists and lists of arrays. Because it’s done in Objective-C it’s pretty fast with large lists, although there’s always a cost in time with the conversion back and forth.

Here’s some simple code from the documentation:

use scripting additions
use framework "Foundation"
use script "BridgePlus"
load framework

set listOrArray to {{1.1, 2}, {3, 4}, {5, 6}}
set theLabels to {"firstLabel", "secondLabel"}
set theResult to current application's SMSForder's subarraysIn:listOrArray asDictionariesUsingLabels:theLabels |error|:(missing value)
ASify from theResult
-->	{{firstLabel:1.1, secondLabel:2}, {firstLabel:3, secondLabel:4}, {firstLabel:5, secondLabel:6}}
theResult as list -- 10.11 only
-->	{{firstLabel:1.1, secondLabel:2}, {firstLabel:3, secondLabel:4}, {firstLabel:5, secondLabel:6}}
theResult as list -- 10.9 and 10.10
-->	{{firstLabel:1.100000023842, secondLabel:2}, {firstLabel:3, secondLabel:4}, {firstLabel:5, secondLabel:6}}

set theLabels to {"firstLabel", "secondLabel"}
set {theResult, theError} to current application's SMSForder's subarraysIn:listOrArray asDictionariesUsingLabels:theLabels |error|:(reference)
if theResult = missing value then error (theError's localizedDescription() as text)

It’s probably worth making the point that the issue with precision of reals in 10.10 and earlier is not specific to records, but happens with ASObjC generally.


(Shane Stanley) #10

And here’s a better example, showing how to use BridgePlus to reverse the process:

use scripting additions
use framework "Foundation"
use script "BridgePlus"
load framework

set listOrArray to {{1.1, 2}, {3, 4}, {5, 6}}
set theLabels to {"firstLabel", "secondLabel"}
set theResult to current application's SMSForder's subarraysIn:listOrArray asDictionariesUsingLabels:theLabels |error|:(missing value)
ASify from theResult
-->	{{firstLabel:1.1, secondLabel:2}, {firstLabel:3, secondLabel:4}, {firstLabel:5, secondLabel:6}}
theResult as list -- 10.11 only
-->	{{firstLabel:1.1, secondLabel:2}, {firstLabel:3, secondLabel:4}, {firstLabel:5, secondLabel:6}}
theResult as list -- 10.9 and 10.10
-->	{{firstLabel:1.100000023842, secondLabel:2}, {firstLabel:3, secondLabel:4}, {firstLabel:5, secondLabel:6}}

set newResult to (current application's SMSForder's subarraysFrom:theResult usingKeys:theLabels outKeys:(missing value) |error|:(missing value)) as list
--> {{1.1, 2}, {3, 4}, {5, 6}}

#11

or in contexts where a library dependency might complicate distribution,
lists ⇄ record by hand, clicked together from re-usable parts:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    set vs to {{1.1, 2}, {3, 4}, {5, 6}}
    set ks to {"firstLabel", "secondLabel"}
    
    -- As a record,
    set rec to mapFromList(zip(ks, vs))
    
    --> {firstLabel:{1.1, 2}, secondLabel:{3, 4}}
    
    -- and back to lists.
    set kvs to assocs(rec)
    
    {map(fst, kvs), map(snd, kvs)}
    
    --> {{"firstLabel", "secondLabel"}, {{1.1, 2}, {3, 4}}}
    
end run


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

-- https://github.com/RobTrew/prelude-applescript

-- assocs :: Map k a -> [(k, a)]
on assocs(m)
    set c to class of m
    if list is c then
        zip(enumFromTo(1, length of m), m)
    else if record is c then
        set dict to (current application's ¬
            NSDictionary's ¬
            dictionaryWithDictionary:(m))
        zip((dict's allKeys()'s ¬
            sortedArrayUsingSelector:"compare:") as list, ¬
            dict's allValues() as list)
    else
        {}
    end if
end assocs


-- mapFromList :: [(k, v)] -> Dict
on mapFromList(kvs)
    set tpl to unzip(kvs)
    script
        on |λ|(x)
            x as string
        end |λ|
    end script
    (current application's NSDictionary's ¬
        dictionaryWithObjects:(|2| of tpl) ¬
            forKeys:map(result, |1| of tpl)) as record
end mapFromList

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

-- fst :: (a, b) -> a
on fst(tpl)
    if class of tpl is record then
        |1| of tpl
    else
        item 1 of tpl
    end if
end fst

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

-- min :: Ord a => a -> a -> a
on min(x, y)
    if y < x then
        y
    else
        x
    end if
end min

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

-- snd :: (a, b) -> b
on snd(tpl)
    if class of tpl is record then
        |2| of tpl
    else
        item 2 of tpl
    end if
end snd

-- Tuple (,) :: a -> b -> (a, b)
on Tuple(a, b)
    {type:"Tuple", |1|:a, |2|:b, length:2}
end Tuple

-- unzip :: [(a,b)] -> ([a],[b])
on unzip(xys)
    set xs to {}
    set ys to {}
    repeat with xy in xys
        set end of xs to |1| of xy
        set end of ys to |2| of xy
    end repeat
    return Tuple(xs, ys)
end unzip

-- zip :: [a] -> [b] -> [(a, b)]
on zip(xs, ys)
    set lng to min(length of xs, length of ys)
    set lst to {}
    repeat with i from 1 to lng
        set end of lst to Tuple(item i of xs, item i of ys)
    end repeat
    return lst
end zip

(Shane Stanley) #12

Notwithstanding all the code above, usually the most efficient way to build lists of records is to do it as you build the list in the first place. If you compare these two repeat loops:

set theText to "one two three four five six seven eight"
set theList to {}
repeat 1000 times
	set end of theList to words of theText
end repeat

and:

set theText to "one two three four five six seven eight"
set theList to {}
repeat 1000 times
	set x to words of theText
	set end of theList to {one:item 1 of x, two:item 2 of x, three:item 3 of x, four:item 4 of x, five:item 5 of x, six:item 6 of x, seven:item 7 of x, eight:item 8 of x}
end repeat

The difference in time taken is less than 5%.


#13

5% run-time or 5% scripting time at the keyboard ?

We are talking milliseconds, or minutes ?

I can certainly imagine contexts – single coder, hundreds or thousands of users, in particular – where run-time can become a relevant and serviceable proxy for ‘efficiency’, (more of a stretch to try using it as a proxy for ‘quality’) but …

do you feel it makes much sense in the case of people writing scripts mainly for themselves?

In that context, I think ‘efficiency’ is in practice more likely to be a function of the scripter’s time.


(Shane Stanley) #14

Yes, I do. Not because of the time factor — I simply make the point that in this case it’s also very efficient where that matters — but because it’s done with a grand total of one extra line of code, rather than a library dependency or a bunch of associated handlers. IMO all the earlier solutions (mine included) are, for many situations, over-thinking the problem.


#15

Got it – it looked as if you were defining ‘efficiency’ as a function of run-time speed, which would obviously be a bit misleading and dysfunctional.

Important, of course, not only to make efficient use of the scripter’s time, but also to minimize any loss of the user/scripter’s time by run-time errors. No computation is slower than one which has come off the road and failed at a sharp corner or unforeseen edge-case.

In the contexts of both scripting-time efficiency and user-time efficiency, I personally prefer to assemble things from pre-fab parts. Endlessly coding from scratch requires more constant vigilance at the wheel than is realistically likely to be feasible or available : -)


(Shane Stanley) #16

I think we’ve got that message. But there’s no one-size-fits-all in scripting. I have no idea what percentage of scripters write stuff only for themselves — it might be most, for all I know — but I do know that there is a substantial number who automate for others, and for whom run-time speed is a significant issue. Indeed, the last line of the post that started this thread was fairly specific: “I’m guessing there’s a faster way.”

I also think there are many others who quite enjoy the exercise of optimizing scripts, especially given that AppleScript can be a relatively slow language. Many of the common idioms are the result of that very exploration.

I’m certainly not telling people that speed matters to them — they can make up their own minds, given their own context. I post the relative speeds for the information of those for whom it does matter, or who are simply interested. In this case, the speed of the various approaches given here for a list of a few hundred items containing a handful of values is going to vary by a couple of orders of magnitude, and that’s significant enough to point out, it seems to me.


#17

I absolutely agree with you on that – everything depends on the practical and organisational context for which we are optimising.

My only resistance is to encouraging a view of run-time speed as a natural or privileged proxy for efficiency or quality.

It does have the merits of being easily measured and easily understood, and, as you say, it can be fun (and instructive) to pursue on a lazy Sunday afternoon, but those very qualities risk making it slightly addictive – sometimes, in practice, at the cost of more fragile reliability, and more wasted time.


(Ed Stockly) #18

I started looking at my script (it’s big) and building the list as a record would be another major overhaul, to basically do one operation (get a list of every item whose item 1 contains x)

So instead, I may simply do that operation in a handler, converting the list to a record and extracting the selected data.

So now the question: Is there a better way to convert a list to a record than stepping through each item in plain vanilla appleScript?


#19

On the scale of my data, passing a match predicate to a findIndices function, and then retrieving matched values or keys by index, turns out to be enough.

Very fast in JS, and serviceable for my purposes in AS too.

on run
    
    set xs to {"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu", "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"}
    
    -- A list of (string key, integer value) pairs:
    
    -- kvs :: [(String, Int)]   
    set kvs to zip(xs, enumFromToInt(1, length of xs))
    
    -- See longKey predicate below
    
    findIndices(longKey, kvs) --> {5, 11, 17, 23}
    
    script
        on |λ|(kv)
            "epsilon" = fst(kv)
        end |λ|
    end script
    findIndices(result, kvs) --> {5, 7}
    
    script matchedKey
        on |λ|(i)
            fst(item i of kvs)
        end |λ|
    end script
    
    script matchedValue
        on |λ|(i)
            snd(item i of kvs)
        end |λ|
    end script
    
    map(matchedKey, findIndices(longKey, kvs)) --> {"epsilon", "lambda", "epsilon", "lambda"}
    
    map(matchedValue, findIndices(longKey, kvs)) --> {5, 11, 17, 23}
end run

-- Search predicate

-- longkey :: (k, v) -> Bool
on longKey(kv)
    5 < length of fst(kv)
end longKey


-- findIndices :: (a -> Bool) -> [a] -> [Int]
on findIndices(p, xs)
    script
        property f : mReturn(p)'s |λ|
        on |λ|(x, i)
            if f(x) then
                {i}
            else
                {}
            end if
        end |λ|
    end script
    concatMap(result, xs)
end findIndices

-- GENERIC FUNCTIONS ---------------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
    set lng to length of xs
    if 0 < lng and class of xs is string then
        set acc to ""
    else
        set acc to {}
    end if
    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

-- enumFromToInt :: Int -> Int -> [Int]
on enumFromToInt(m, n)
    if m ≤ n then
        set lst to {}
        repeat with i from m to n
            set end of lst to i
        end repeat
        return lst
    else
        return {}
    end if
end enumFromToInt

-- fst :: (a, b) -> a
on fst(tpl)
    if class of tpl is record then
        |1| of tpl
    else
        item 1 of tpl
    end if
end fst

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

-- min :: Ord a => a -> a -> a
on min(x, y)
    if y < x then
        y
    else
        x
    end if
end min

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

-- snd :: (a, b) -> b
on snd(tpl)
    if class of tpl is record then
        |2| of tpl
    else
        item 2 of tpl
    end if
end snd

-- Tuple (,) :: a -> b -> (a, b)
on Tuple(a, b)
    {type:"Tuple", |1|:a, |2|:b, length:2}
end Tuple

-- zip :: [a] -> [b] -> [(a, b)]
on zip(xs, ys)
    set lng to min(length of xs, length of ys)
    set lst to {}
    repeat with i from 1 to lng
        set end of lst to Tuple(item i of xs, item i of ys)
    end repeat
    return lst
end zip

(Shane Stanley) #20

You could try the methods here, but my gut feeling is no.