Access js object methods and properties without writing JS bindings... ...or just generate mutable JS object FFI (without any codegen) from the type.
In PureScript we usually write FFI to object methods by implementing dedicated functions on both sides. It seems that we can provide set of generic helpers which without sacrificing the performance which are able to bind to properties and methods of a JS object (by using "uncurried" approach similar to Effect.Uncurried from purescript-effect under the hood).
Let's imagine that we have a simple counter prototype defined on the JS side and we expose an "effectful" function which creates an instance for us (we are not able to use new directly from PS side):
exports.counter=(function(){letCounter=function(){this.value=0;};Counter.prototype.increase=function(){this.value++;};Counter.prototype.decrease=function(){this.value--;};// I'm working to cover constructors as well soon.// At the moment we need to expose a function ourselves.returnfunction(){returnnewCounter();};})();Now we should be able to bind to this interface using generic helpers provided by this library:
importPreludeimportData.Newtype (classNewtype)
importEffect (Effect)
importEffect.Aff (launchAff_)
importEffect.Class (liftEffect)
importJS.Object (EffectMth0, EffectMth1, EffectProp, JSObject, runEffectMth0, runEffectMth1, runEffectProp)
importJS.Object.Generic (mkFFI, mkNewtypedFFI)
importTest.Spec (describe, it)
importTest.Spec.Assertions (shouldEqual)
importTest.Spec.Reporter (consoleReporter)
importTest.Spec.Runner (runSpec)
importType.Prelude (Proxy(..))
importType.Row (type (+))
typeCounter=JSObject (increase::EffectMth0Unit, decrease::EffectMth0Unit, value::EffectPropInt)
foreignimportcounter::EffectCounter-- You can generate this FFI record during the compilation time.-- There is no runtime footprint over the manual binding.-- Type signature is derived automatically but because we don't-- use newtype... yet it would be fully expanded by default._Counter::{increase::Counter->EffectUnit
, decrease::Counter->EffectUnit
, value::EffectInt}
_Counter = mkFFI (Proxy::ProxyCounter)
-- If you want you can use newtypes as well.-- Here is a binding for hypothetical Person `JSObject`.newtypePerson = Person
( JSObject
( firstName :: EffectPropString
, setFirstName :: EffectMth1StringUnit
, lastName :: EffectPropString
, setLastName :: EffectMth1StringUnit
)
)
derive instanceNewtypePerson__Person::{firstName::Person->EffectString
, lastName::Person->EffectString
, setFirstName::Person->String->EffectUnit
, setLastName::Person->String->EffectUnit}
_Person = mkNewtypedFFI (Proxy::ProxyPerson)
-- You can also use lower level functions to construct bindings yourself.increase::Counter->EffectUnit
increase = runEffectMth0 (Proxy::Proxy"increase")
decrease::Counter->EffectUnit
decrease = runEffectMth0 (Proxy::Proxy"increase")
value::Counter->EffectInt
value = runEffectProp (Proxy::Proxy"value")
main::EffectUnit
main = launchAff_ $ runSpec [ consoleReporter ] do
describe "JS.Object"do
it "property access"do
c <- liftEffect counter
v <- liftEffect $ value c
v `shouldEqual`0
it "method call"do
v <- liftEffect $ do
c <- counter
increase c
increase c
v <- value c
pure v
v `shouldEqual`2There are two nice properties of this generic method of binding to JS object:
whenever we feed the "method name proxy" to the one of
runEffectMth*helpers there is no additional overhead - we get back a function which is not dependent on any type class dict and is only passing the rest of the arguments to the uncurried object method by using standardrunEffectFn*under the hood.we can use the same binding functions to different objects as long as they share a particular method signature / interface. We can think about the row as the TS interface and about the particular object as an instance which implements it. The above example could be written as:
typeIncreaseInterfacer= ( increase::EffectMth0Unit | r)
typeDecreaseInterfacer= (decrease::EffectMth0Unit | r)
typeValueInterfacer= (value::EffectPropInt | r)
increase::forallr. JSObject (IncreaseInterfacer) ->EffectUnit
increase = runEffectMth0 (Proxy::Proxy"increase")
decrease::forallr. JSObject (DecreaseInterfacer) ->EffectUnit
decrease = runEffectMth0 (Proxy::Proxy"decrease")
value::forallr. JSObject (ValueInterfacer) ->EffectInt
value = runEffectProp (Proxy::Proxy"value")
typeCounter=JSObject (IncreaseInterface + DecreaseInterface + ValueInterface + ())
$ spago --config devel.dhall test