(Branching from a discussion with Phil http://forum.latenightsw.com/t/converting-an-nsattributedstring-into-an-html-string/1048/14)
Map filter and ‘reduce’ or ‘fold’ are useful when we need to assemble scripts quickly for ourselves, reducing the risks of haste and accident that loops are heir to.
They are essentially pasteable pre-tested loops of generic patterns:
- map for obtaining transformed lists of unchanged length
- fold (or ‘reduce’ in JS) for obtaining single values from a list
- filter for defined pruning
First some examples, and then a note on doing it in AppleScript.
Examples:
on double(x)
x * 2
end double
on half(x)
x / 2
end half
on even(x)
x mod 2 = 0
end even
on add(a, b)
a + b
end add
on mult(a, b)
a * b
end mult
on sum(xs)
foldl(add, 0, xs)
end sum
on product(xs)
foldl(mult, 1, xs)
end product
-- TEST ------------------------------------------------------------------
on run
map(double, {1, 2, 3, 4, 5})
--> {2, 4, 6, 8, 10}
map(half, {1, 2, 3, 4, 5})
--> {0.5, 1.0, 1.5, 2.0, 2.5}
filter(even, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
--> {2, 4, 6, 8, 10}
sum({1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
--> 55
product({1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
--> 3628800
end run
-- GENERICS --------------------------------------------------------------
-- filter :: (a -> Bool) -> [a] -> [a]
on filter(f, xs)
tell mReturn(f)
set lst to {}
set lng to length of xs
repeat with i from 1 to lng
set v to item i of xs
if |λ|(v, i, xs) then set end of lst to v
end repeat
return lst
end tell
end filter
-- 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
-- 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
Map fold and filter in AppleScript
The generality and reusability of map, fold and filter depend on our passing other functions as arguments to them.
Plain AppleScript handlers are not in themselves ‘first-class’ functions that can fully survive being passed as values like this, and at first this looks like an obstacle.
Fortunately, however:
- Script objects with handlers are first class objects, and
- we can wrap any handler in a script at run-time.
The oddly named mReturn function which I personally use to do this might be named more clearly as something like scriptWrapped(handler)
-- 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
- If the argument is already a script, it is returned unchanged
- otherwise, it is assumed to be a handler, and the return value is a script which has that handler as a property
- the original name of the handler is discarded, and something more anonymous, like lambda or |λ| is used instead.
This allows us to write things like map(double, {1,2,3,4,5})
– defining map in a way which script-wraps any handler that is passed as its first argument:
-- 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
In short, our map is just a pre-packaged, pre-tested and reusable loop, in which any handler passed as the argument f is applied in turn to each item in the list xs.
A few more details
Additional arguments for |λ|
Inside the map loop (and equally inside the filter and foldl loops), you may notice that the list transforming function (passed as f, and applied as |λ|, can have up to 3 arguments, rather than just one.
This echoes the pattern used by JavaScript’s Array.map method,
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
in which the function passed to map can refer not only to the current element in the list, but also to the index of that element (zero-based in JS, but 1-based in AS), and additionally to the whole list.
Passing scripts rather than handlers to map, filter, fold
Defining these generic loop patterns in terms of an mReturn or scriptWrapped function allows us to pass either handlers or pre-existing scripts as arguments.
We could, for example write:
on run
set xs to {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
script
on |λ|(x)
x * 2
end |λ|
end script
map(result, xs)
--> {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
-- or
script double
on |λ|(x)
x * 2
end |λ|
end script
map(double, xs)
--> {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
end run
-- 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
Why and when ?
These patterns work well, in my experience, when I am writing code for my own use.
- Scripts can be quickly assembled from reusable generics (I maintain parallel libraries of c. 350 generic functions in AS and JS, and I’m beginning to assemble one in Swift),
- The way they click together is rather simple and predictable - and I find that I spend much less time in debugging as a result
- Using the same generic patterns across different languages eases a bit of the cognitive burden for me, and I also find that I use the functions as reference manuals. I may not always use the simplest ones (It can be faster, on occasions when that’s helpful, to just inline the code, but if I know how do do something in one language, and have forgotten the details in the other, I tend to just look up the familiar generic function, and glance at its implementation in the language that I’m writing in.
When not to use these patterns ?
When particular economics or practicalities make human time (hand-coding at a lower level, stepping through debuggers when a hand-written mutation has found some puzzling edge condition) less expensive than execution time, then you should always be able to manually soup up your inner loops.
Even then (and even I sometimes want a diagram, for example, to generate more swiftly), I find that it can work well to first build a pre-fab structure with (map and fold and other generics) and then replace some of the inner iterations with something hand written.
In practice tho, in my own context, I am increasingly finding (with OmniGraffle etc now starting to acquire their own JSContexts) that when maxed-out zippiness at any cost is what I want, I may end up doing it in JS).
(Not always, of course, where no JSContext is to hand, AS is, as we know, still much faster at batch-setting properties).
As for ‘FP’, well, others take different views but I happen to be a bit agnostic about that notion – is there really a clear dividing line ? All coding involves functions. Even sequences and semi-colons can be interpreted as functions …
But I do know that map filter and fold/reduce make my life a bit easier in any language