ObjC bindFunction equivalent for Math functions in AppleScript?


#1

The JS Automation object provides ObjC.bindFunction(), which enables us to pull in functions that are not automatically bridged.

Is there something analogous in the AppleScript interface to ObjC which would enable us to pull in trigonometric functions, for example ?

Not needed in JS, which has a Math library, but you can do things like:

(() => {

    ['sin', 'cos'].forEach(
        k => ObjC.bindFunction(k, ['double', ['double']])
    );

    return ['sin', 'cos'].map(f => $[f](0.75));
    // [ 0.6816387600233342, 0.7316888688738209]
})();

which would fill a gap quite well in AppleScript.


(Shane Stanley) #2

No.

For trig functions you can use javascript:

use AppleScript version "2.4"
use framework "Foundation"
use framework "JavaScriptCore"

set theContext to current application's JSContext's new()
(theContext's evaluateScript:"Math.sin(0.75)")'s toDouble()

or BridgePlus:

use AppleScript version "2.4" 
use framework "Foundation"
use libVariable : script "BridgePlus" version "1.3.2"

load framework
(current application's SMSForder's sinValueOf:0.75) as real

#3

use framework “JavaScriptCore”

Brilliant. I hadn’t thought of that. Thank you !


#4

I haven’t measured the cost of repeated JSContext.alloc.init – perhaps it’s not exorbitant enough to warrant much circumvention ?

Here is a first experiment in reusing the same JSContext across several calls from AppleScript, though it does require manual release of a global AS variable at the end.

use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions

global gJSC -- NB If this is used, must be set to missing value before end of script

-- USING A JSCONTEXT ----------------------------------------------------

-- cos :: Num -> Num
on cos(n)
    jsMath("cos", n)
end cos

-- sin :: Num -> Num
on sin(n)
    jsMath("sin", n)
end sin

-- percentEncode :: String -> String
on percentEncode(s)
    jsEval("encodeURIComponent('" & s & "')")
end percentEncode

-- TEST -------------------------------------------------------------------
on run
    
    set xs to ap({sin, cos}, {0.3, 0.5, 0.75})
    
    set ys to ap({percentEncode}, {" with spaces ", "reserved:@&=+$,/?#[]"})
    
    -- NB (*** to make the script saveable ***)
    set gJSC to missing value -- if declared as global (see top)
    
    return {xs, ys}
    
    -- (*new JSC*)
    -- Result:
    -- {{0.295520206661, 0.479425538604, 0.681638760023, 0.955336489126, 0.87758256189, 0.731688868874}, {"%20with%20spaces%20", "reserved%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D"}}
end run


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

-- bindJSC :: JSC a -> (a -> JSC b) -> JSC b
on bindJSC(mJSC, mf)
    set strJS to |λ|(value of mJSC) of mReturn(mf)
    {type:"JSC", jsc:mJSC's jsc, value:¬
        unwrap((mJSC's jsc's evaluateScript:(strJS))'s toObject())}
end bindJSC

-- Applies wrapped functions to wrapped values, 
-- for example applying a list of functions to a list of values
-- or applying Just(f) to Just(x), Right(f) to Right(x), etc
-- ap (<*>) :: Monad m => m (a -> b) -> m a -> m b
on ap(mf, mx)
    if class of mx is list then
        apList(mf, mx)
    else if class of mf is record then
        set ks to keys(mf)
        if ks contains "type" then
            set t to type of mx
            if t = "Either" then
                apEither(mf, mx)
            else if t = "Maybe" then
                apMaybe(mf, mx)
            else if t = "Tuple" then
                apTuple(mf, mx)
            else
                missing value
            end if
        else
            missing value
        end if
    end if
end ap

-- e.g. [(*2),(/2), sqrt] <*> [1,2,3]
-- -->  ap([dbl, hlf, root], [1, 2, 3])
-- -->  [2,4,6,0.5,1,1.5,1,1.4142135623730951,1.7320508075688772]

-- Each member of a list of functions applied to
-- each of a list of arguments, deriving a list of new values
-- apList (<*>) :: [(a -> b)] -> [a] -> [b]
on apList(fs, xs)
    set lst to {}
    repeat with f in fs
        tell mReturn(contents of f)
            repeat with x in xs
                set end of lst to |λ|(contents of x)
            end repeat
        end tell
    end repeat
    return lst
end apList


-- jsEval :: String -> Num -> Num
on jsEval(strJSCode)
    try
        type of gJSC = "JSC"
    on error
        log ("new JSC")
        set gJSC to pureJSC("")
    end try
    script mf
        on |λ|(_)
            strJSCode
        end |λ|
    end script
    value of bindJSC(gJSC, mf)
end jsEval

-- JS Math function name -> Number -> Number
-- jsMath :: String -> Num -> Num
on jsMath(k, n)
    try
        type of gJSC = "JSC"
    on error
        log ("new JSC")
        set gJSC to pureJSC("")
    end try
    script mf
        on |λ|(_)
            ("Math." & k & "(" & n as string) & ")"
        end |λ|
    end script
    value of bindJSC(gJSC, mf)
end jsMath


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

-- pureJSC :: a -> {type::String, jsc::JSContext, value::a}
on pureJSC(x)
    {type:"JSC", jsc:current application's JSContext's new(), value:x}
end pureJSC

-- unwrap :: NSObject -> a
on unwrap(objCValue)
    if objCValue is missing value then
        missing value
    else
        set ca to current application
        item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

#5

Or this, though perhaps a bit clumsy and unfamiliar :slight_smile:

use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions


-- A single JSContext instance threaded through successive applications of additional
-- functions to output values

-- Needs to be released manually at end of script
global gJSC

on run
    set v to pureJSC(7) -- 7
    
    set w to bindJSC(v, tanJS) -- tan(7)
    set x to bindJSC(w, dblJS) -- tan(7) * 2
    set y to bindJSC(x, invJS) -- 1/(tan(7) * 2)
    set z to bindJSC(y, sqrtJS) -- sqrt(1/(tan(7) * 2))
    
    -- Equivalent to:
    --     set z to ¬
    --        bindJSC(bindJSC(bindJSC(¬
    --            bindJSC(pureJSC(7), tanJS), dblJS), invJS), sqrtJS) -- sqrt(1/(tan(7) * 2))
    
    
    script showValue
        on |λ|(x)
            value of x
        end |λ|
    end script
    set xs to map(showValue, {v, w, x, y, z})
    
    -- REFERENCES RELEASED
    set gJSC to null
    set {v, w, x, y, z} to {null, null, null, null, null}
    
    -- RESULT
    return xs
    --> {7, 0.871447982724, 1.742895965448, 0.573757711203, 0.757467960513}
end run

on cosJS(x)
    "Math.cos(" & x & ")"
end cosJS

on sinJS(x)
    "Math.sin(" & x & ")"
end sinJS

on tanJS(x)
    "Math.tan(" & x & ")"
end tanJS

on sqrtJS(x)
    "Math.sqrt(" & x & ")"
end sqrtJS

on dblJS(x)
    "2 * " & x
end dblJS

on invJS(x)
    "1/" & x
end invJS


-- Higher order functions for wrapped composition

-- pureJSC :: a -> {type::String, jsc::JSContext, value::a}
on pureJSC(x)
    try
        gJSC's evaluateScript
    on error
        set gJSC to current application's JSContext's new()
        log "New JSC"
    end try
    {type:"JSC", jsc:gJSC, value:x}
end pureJSC

-- bindJSC :: JSC a -> (a -> JSC b) -> JSC b
on bindJSC(mJSC, mf)
    set strJS to |λ|(value of mJSC) of mReturn(mf)
    {type:"JSC", jsc:mJSC's jsc, value:¬
        unwrap((mJSC's jsc's evaluateScript:(strJS))'s toObject())}
end bindJSC

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

-- unwrap :: NSObject -> a
on unwrap(objCValue)
    if objCValue is missing value then
        missing value
    else
        set ca to current application
        item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

(Shane Stanley) #6

Unless I misunderstand you, Script Debugger’s Persistent Properties setting make this a non-issue.

(Your first script doesn’t run here.)

It’s likely to add considerably to the time taken. But then, you’re already adding a significant amount of overhead.

If I compare your second version with something straight forward:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions
property theContext : missing value

set a to 7
set w to tanJS(a)
set x to dblJS(w)
set y to invJS(x)
set z to sqrtJS(y)
return {a, w, x, y, z}

on myContext()
	if theContext = missing value then set theContext to current application's JSContext's new()
	return theContext
end myContext

on cosJS(x)
	(myContext()'s evaluateScript:("Math.cos(" & x & ")"))'s toDouble()
end cosJS

on sinJS(x)
	(myContext()'s evaluateScript:("Math.sin(" & x & ")"))'s toDouble()
end sinJS

on tanJS(x)
	(myContext()'s evaluateScript:("Math.tan(" & x & ")"))'s toDouble()
end tanJS

on sqrtJS(x)
	(myContext()'s evaluateScript:("Math.sqrt(" & x & ")"))'s toDouble()
end sqrtJS

on dblJS(x)
	(myContext()'s evaluateScript:("2 * " & x))'s toDouble()
end dblJS

on invJS(x)
	(myContext()'s evaluateScript:("1/" & x))'s toDouble()
end invJS

in trials of 100 runs here the difference is significant: the former takes more than six times as long.


(Jim Underwood) #7

Thanks, Shane. This is very cool.
Does using the framework "JavaScriptCore" make available all of the JavaScript core functions, like the Regexp methods?


(Shane Stanley) #8

It should do. There are two wrinkles: depending on what you want to do, building the javascript on the fly can be a bit complex and inflexible (you’d probably need a handler for each form of search and/or replace), and it’s probably going to end up considerably slower than sticking to NSRegularExpression.


(Jim Underwood) #9

Thanks Shane. I’ll keep that in mind.


#10

Thank you very much for taking the time to test – that’s been really helpful.

Script > Persistent Properties

That’s very encouraging and I hadn’t spotted it. (Is there a corresponding option in anything like OSACompile ?)

Persistence a non-issue.

You may well be right – what I remain unclear about is how or when the system releases the JSContext. Even if we have saved as .applescript text, the JSContext clearly persists between Applescript runs (tho not, we hope, between recompilations).

For example, the second run of the following script returns 0 ( toInt32()'s version of undefined) where the first run returns 42;

(The xxxxx constant persisted in the JSContext between runs, and constants can’t be reassigned, so there was an error, and an undefined return)

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions

property gJSC : missing value

on run
    set valueTest to (JSC()'s evaluateScript:("const xxxxx = 42; xxxxx"))'s toInt32()
end run

on JSC()
    if gJSC is missing value then ¬
        set gJSC to current application's JSContext's new()
    gJSC
end JSC

It’s likely to add considerably to the time taken. But then, you’re already adding a significant amount of overhead.

Indeed – In fact looking at it again after your test, I realised (now amended above), that pureJSC was actually creating a new JSContext() at every stage of evaluation.

Even so, and particularly once we are using only one JSContext, I am very impressed by the speed. More than enough for my purposes with Applescript, even with quite a lot of wrapping and abstraction layered on top of it …

(For more trig-intensive generation of diagrams etc, I would probably use an embedded JS interpreter in something like OmniGraffle, Sketch, or Diagram anyway - not sure that AppleScript would quite be the right instrument for those tasks anyway …)


Incidentally, another thing that I haven’t tested is the relative costs of type-specific (toDouble() etc) conversions from JSValue, vs type-agnostic toObject(), followed by that unwrap function, to allow for a fuller spectrum of return values.


#11

Forgot to mention this – FWIW on macOS 10.12.6 that script is running fine in SD, and Script Editor, and with osascript etc.

I’m sure you copy-pasted the whole thing, and I wonder whether it’s bumping into another 10.13 issue there ?


(Shane Stanley) #12

The context is retained for the life of the underlying AppleScript component, and in an editor (these days) the life of an AppleScript component is from one compile to the next. Turning off Persistent Properties means Script Debugger saves the script without top-level values.

The real-world repercussion of storing pointers in top-level variables is that you can’t rely on persistence of any properties – after you run an applet for instance, the normal attempt to save with updated top-level values fails (silently). But if you’re doing something like codesigning, that’s the reality anyway.

Are you sure? The amended version is no quicker here in tests. I’m sure the wrapping and abstraction is a matter of taste, but it’s also likely to produce code that can’t be debugged.

It’s obviously adding a tiny bit of overhead. I just can’t see the point when you know what type the result is.


(Shane Stanley) #13

I’m running 10.12.6, and I tried in Script Debugger and Script Editor. I copied and pasted twice to be sure, and I’ve just repeated the exercise with the same result. The error is "Can’t make «script» into type Unicode text." number -1700. I’m not sure where it happens – as I said, that construction doesn’t lend itself to debugging.


#14

The context is retained for the life of the underlying AppleScript component, and in an editor (these days) the life of an AppleScript component is from one compile to the next.

Thanks that’s a helpful clarification.

Given that picture, I think I’ll still maintain a habit of clearing the reference manually before the script ends. Cross-pollution of runs by JSContext persistence is not, of course, limited to the issue of const declarations – unless we disable Persistent Properties in Script Debugger (not an option if I share a script with someone more likely to use Script Editor), the following returns a different result on every successive run:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions

property gJSC : missing value

on run
    set valueTest to (JSC()'s evaluateScript:("var x = x || 0; x += 1"))'s toInt32()
end run

on JSC()
    if gJSC is missing value then ¬
        set gJSC to current application's JSContext's new()
    gJSC
end JSC

I just can’t see the point when you know what type the result is.

I agree with you in those contexts (though, of course, even some Math functions are partial, and error information can perish in some over-optimistic type conversions of results) but it’s also quick and useful to do many things through a general-purpose jsEval of some kind.

that construction doesn’t lend itself to debugging

I do hear what you are saying - I mainly use Script Debugger for object exploration and timing - I don’t do much debugging of mutation and iteration.

(The trade-off, in my context, is that avoiding mutation and structuring by composition reduces debugging time (and boosts code reuse) very noticeably. It’s certainly quite a different approach though, and when the issues of mutation retreat from the foreground, the methods of debugging are certainly a bit different, and less directly supported by SD)

Depends on what we are optimising for I guess – probably not all that much mileage in generalizing too confidently in either direction.


#15

The alternative is to localise the JSContext name bindings by wrapping all code in an immediately executed anonymous function.

This variant, even with persistent properties enabled, always returns the same result:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions

property gJSC : missing value

on run
    set valueTest to (JSC()'s ¬
        evaluateScript:("(() => { var x = x || 0; return x += 1;})()"))'s toInt32()
end run

on JSC()
    if gJSC is missing value then ¬
        set gJSC to current application's JSContext's new()
    gJSC
end JSC

#16

May see this differently by the end of the week, but for the moment, I seem to be finding workable middle ground in:

  1. Avoiding the use of a property or global reference by wrapping the JSContext reference in script ... end script
  2. Returning all data from the JSContext as a predictable (NSDictionary) type, with separate type and value keys.

Avoiding the property / global reference should make it a bit less accident-prone on the ‘Can’t save script containing pointers’ front, and a script reference still gives us reuse of a single JSContext() ( subject to the caveats about persistent JS name-bindings within a single AS session ).

NSDictionary -> record lets us know what to expect at the AppleScript end, and allows for a bit more clarity or specificity about JS return types like NaN and undefined;

 use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions

on run
    set ca to current application
    script math
        property JSC : ca's JSContext's new()
        
        -- eval :: String -> {jsType::String, value::a}
        on eval(strJS)
            set mb to JSC's evaluateScript:("(() => { const v = (() => " & ¬
                "{ return " & strJS & "})(), t = typeof v; " & ¬
                " return {jsType: t !== 'object' ? (t !== 'undefined' ? t : '⊥') : " & ¬
                "(Array.isArray(v) ? 'array' : 'object')," & ¬
                " value: t !== 'function' ? ( t !== 'undefined' ? v : null) : " & ¬
                "v.toString()}})()")
            if mb's isUndefined() then
                {jsType:"⊥", value:missing value}
            else
                mb's toDictionary() as record
            end if
        end eval
        
        -- mathFn :: String -> Number
        on mathFn(k, n)
            set rec to eval(("Math." & k & "(" & n as string) & ")")
            if jsType of rec is "number" then
                value of rec
            else
                missing value
            end if
        end mathFn
        
        -- MATH FUNCTIONS ----------------------------------------------------
        
        -- sin :: Real -> Real
        on sin(x)
            mathFn("sin", x)
        end sin
        
        -- cos :: Real -> Real
        on cos(x)
            mathFn("cos", x)
        end cos
        
        -- tan :: Real -> Real
        on tan(x)
            -- but xs == Pi/2 may deserve special handling
            mathFn("tan", x)
        end tan
        
        -- sqrt :: Real -> Real
        on sqrt(x)
            -- NaN where 0 > x
            mathFn("sqrt", x)
        end sqrt
        
        -- logBase :: Real -> Real -> Real
        on logBase(x, y)
            set rec to eval((("Math.log(" & y as string) & ¬
                ")/Math.log(" & x as string) & ")")
            if jsType of rec is "number" then
                value of rec
            else
                missing value
            end if
        end logBase
    end script
    
    -- TEST ---------------------------------------------------------
    tell math
        {logBase(2, 8), sin(0.25), cos("frog"), tan(pi / 4), ¬
            tan(pi / 2), sqrt(-8), sqrt(8), ¬
            eval("Object.getOwnPropertyNames(this)")}
    end tell
    
end run
{3.0, 0.247403959255, missing value, 0.999999999999, -9.67009938079218E+12, NaN, 2.828427124746, {value:{"Infinity", "NaN", "undefined", "parseFloat", "isNaN", "isFinite", "escape", "unescape", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "EvalError", "ReferenceError", "SyntaxError", "URIError", "Proxy", "JSON", "Math", "Atomics", "console", "Int8Array", "Int16Array", "Int32Array", "Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array", "Float32Array", "Float64Array", "DataView", "Set", "Date", "Boolean", "Number", "WeakMap", "WeakSet", "parseInt", "Object", "Function", "Array", "RegExp", "RangeError", "TypeError", "ArrayBuffer", "SharedArrayBuffer", "String", "Symbol", "Error", "Map", "Promise", "eval", "Intl", "Reflect"}, jsType:"array"}}

(Shane Stanley) #17

On more practical matters, it’s worth pointing out that all the functions you’ve listed can be calculated directly in AppleScript, and therefore faster.


#18

Of course – and for speed, which is rarely a sensible priority in most of the things I’m doing, I would do it entirely in a JSContext anyway.

For occasional math, mileage will vary, but I’d personally rather import well-tested functions from outside – takes less human time, and gives more human confidence.

Seems a good division of labour to me – I feel that David Ricardo would have approved.


(Jim Underwood) #19

Shane, could you please explain how?
I’ve done some research, and don’t find any native trig functions for AppleScript. In fact, most hit point to using Satimage OSAX.

For both speed and convenience, Satimage would seem to be a good solution.


(Shane Stanley) #20

Well sqrt is simply ^ 0.5. There are other handlers here

https://macosxautomation.com/applescript/sbrt/pgs/sbrt.02.htm

that should still work, although they’re designed to work with degrees rather than radians. You can also derive cos from sin using (1 - sinX ^ 2) ^ 0.5, so you really only need a sin handler.

It’s not an exhaustive list by any means, but it covers the ones above. I don’t know their precision, although I used a couple of them in scripts many (many!) years ago (for drawing pie charts in Adobe Illustrator), and they worked fine.