Detecting at run-time how many arguments a handler expects?

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

Alerted by Stan Cleveland’s post

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.

Anyway, this is a taste of what’s coming:

2 Likes

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.

And if you do want optional parameters, you can add a terminology file.

In fact, I reckon the ability to have optional parameters is the biggest plus about adding terminology.

The ideal, I guess, is to be able to uncurrycurry 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