In JavaScript can have fields of objects, methods, functions, arrays etc etc. and we need to cover all these cases. Let's start with a little case study. We want to manipulate the DOM. Here is a little HTML snippet to begin with:
<html> <head> </head> <body> <div id="test">Foobar</div> <script src="test.js"></script> </body> </html>Now we want to replace the text of the Node with the id
test
module Main
data HTMLElement : Type where
Elem : Ptr -> HTMLElement
data NodeList : Type where
Nodes : Ptr -> NodeList
query : String -> IO NodeList
query q = do
e <- mkForeign (FFun "document.querySelectorAll" [FString] FPtr) q
return (Nodes e)
item : NodeList -> Int -> IO HTMLElement
item (Nodes p) i = do
i <- mkForeign (FFun ".item" [FPtr, FInt] FPtr) p i
return (Elem i)
getId : HTMLElement -> IO String
getId (Elem p) = mkForeign (FFun ".id" [FPtr] FString) p
setText : HTMLElement -> String -> IO ()
setText (Elem p) s =
mkForeign (FFun ".textContent=" [FPtr, FString] FUnit) p s
main : IO ()
main = do
e <- query "#test"
i <- item e 0
s <- getId i
setText i (s ++ ": SUPERFOO!!!")
In this example I'm using the Ptr type for raw JavaScript values and wrap them in ADTs. The interesting part is in the definition of the foreign functions. Functions starting with "." are methods or fields, that means that the first argument of the function is handled as and object and the function call gets translated into a method or field name. Functions ending with "=" are turned into assignments
Another thing we have to consider is that sometimes we want to refer to a method with no arguments, therefore we have to distinguish them from reading a field. In our example we read the value of the field
id
. If we wanted to turn that into a method call we need to declare it like this:
mkForeign (FFun ".id" [FPtr, FUnit] FString) p ()
We simply add an argument of type FUnit to the argument list and apply ().Operations on arrays are declared like this:
mkForeign (FFun ".id[]" [FPtr, FInt] FString)
mkForeign (FFun ".id[]=" [FPtr, FInt, FString] FString)
The second argument is treated as an indexWorking with the FFI is still dangerous, I'm currently unaware of a different way to do this without changing Idris' FFI which is something I don't want to. Another thing I don't want to do is making the FFI overly complex, it should be very low level and provide the basic building blocks for interacting with JavaScript. Anyway, patches are welcome ^^
One thing that might be worth considering is a way to declare safe foreign calls that do not need to be wrapped in IO.