Iterating through a list of handlers for automated testing, I would like to be able to detect, at runtime, how many arguments each handler expects.
I can see a way of doing this by counting commas in error messages (see below), but perhaps there is something more solid and straightforward ?
Does an AppleScript handler generally have any properties that we can find a way of reading at run-time ?
-- argvLength :: Handler -> Int
on argvLength(h)
try
mReturn(h)'s |λ|()
0
on error errMsg
set {dlm, my text item delimiters} to {my text item delimiters, ","}
set xs to text items of errMsg
set my text item delimiters to dlm
length of xs
end try
end argvLength
-- TEST: How many arguments does each handler expect ?
on run
map(argvLength, {hello, succ, min, zipWith})
--> {0, 1, 2, 3}
end run
-- SAMPLE HANDLERS FOR TESTING -------------------------------------------
-- hello :: () -> String
on hello()
"hello"
end hello
-- succ :: Int -> Int
on succ(x)
x + 1
end succ
-- min :: Ord a => a -> a -> a
on min(x, y)
if y < x then
y
else
x
end if
end min
-- zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
on zipWith(f, xs, ys)
set lng to min(length of xs, length of ys)
if lng < 1 then return {}
set lst to {}
tell mReturn(f)
repeat with i from 1 to lng
set end of lst to |λ|(item i of xs, item i of ys)
end repeat
return lst
end tell
end zipWith
-- 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 :: 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
to some of the handler properties exposed by the Script Debugger’s document model, I wondered if the source property might yield a more civilised route to reading the arity of a handler.
Not sure that I can, in fact, see a route though – quite understandably (in analogy to store script applied to a handler rather than a script object), it returns the source of the body of the handler, separated from its argument declaration line.
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
-- handlerSource :: Handler -> String
on handlerSource(h)
tell application "Script Debugger"
tell document (my takeFileName(POSIX path of ((path to me))))
tell script handler (my handlerName(h))
its source text
end tell
end tell
end tell
end handlerSource
-- TEST ----------------------------------------------------------------
on run
handlerSource(my splitOn)
end run
-- handlerName :: Handler -> String
on handlerName(h)
tell mReturn(h)
try
|λ|()
|λ|(missing value)
on error e
set {dlm, my text item delimiters} to {my text item delimiters, space}
set xs to text items of e
set my text item delimiters to dlm
text 1 thru -2 of (last item of xs)
end try
end tell
end handlerName
-- GENERIC FUNCTIONS ------------------------------------------------------------------
-- 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
-- splitOn :: String -> String -> [String]
on splitOn(strDelim, strMain)
set {dlm, my text item delimiters} to {my text item delimiters, strDelim}
set xs to text items of strMain
set my text item delimiters to dlm
return xs
end splitOn
-- takeFileName :: FilePath -> FilePath
on takeFileName(strPath)
if strPath ≠ "" and character -1 of strPath ≠ "/" then
item -1 of splitOn("/", strPath)
else
""
end if
end takeFileName
This all reminded me of code that got lost with the move from SD4.5 to SD5 (one of Script Debugger’s many rewrites) that would return the parameters of a handler. For the next Script Debugger 7 beta build, I’ve re-introduced this.
But be warned, generating AppleScript handler calls is a non-trivial task. Handlers accept four types of parameters (direct object, positional, keyword and named). Then, there are different syntaxes for expressing positional parameters.
I thought that, to avoid injection dependency, handlers should be written to accept a record of arguments so you could add additional args as needed without having to hunt down every mention of the handler
on handler(args)
try
set param to something of args
end
end
This is certainly one approach. Given that AppleScript does not support the notion of optional parameters, this design deals with that as well. However, you loose the formality and expressiveness of declaring a handler with its parameters defined in the handler declaration. Its a trade-off.
The ideal, I guess, is to be able to uncurry ⇄ curry functions on the fly.
I notice that people started to experiment with this pretty quickly in Swift:
Not entirely beyond the reach of Applescript, for some purposes:
-- splitOn :: String -> String -> [String]
on splitOn(strDelim, strMain)
set {dlm, my text item delimiters} to {my text item delimiters, strDelim}
set xs to text items of strMain
set my text item delimiters to dlm
return xs
end splitOn
on run
set tuple_splitOn to uncurry(splitOn)
set tpl to Tuple(space, "There are various approaches")
log tpl
return tuple_splitOn's |λ|(tpl)
end run
-- Returns a function on a single tuple (containing 2 arguments)
-- derived from an equivalent function with 2 distinct arguments
-- uncurry :: (a -> b -> c) -> ((a, b) -> c)
on uncurry(f)
script
property mf : mReturn(f)'s |λ|
on |λ|(pair)
mf(|1| of pair, |2| of pair)
end |λ|
end script
end uncurry
-- curry :: ((a, b) -> c) -> a -> b -> c
on curry(f)
script
on |λ|(a)
script
on |λ|(b)
|λ|(a, b) of mReturn(f)
end |λ|
end script
end |λ|
end script
end curry
-- 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}
end Tuple