-
Notifications
You must be signed in to change notification settings - Fork 89
Foreign function interface
Here's an example of a FFI declaration:
max :: Double -> Double -> Double
max = ffi "Math.round(%1,%2)"
%1,%2,..
corresponds to the arguments you specify in the type.
%*
splats all arguments, it will produce %1,%2,..
which can be useful for a declaration ffi "(function (a,b) { return Math.round(a,b); })(%*)"
Be careful when declaring FFI types because Fay can not verify that they are correct.
A FFI function often has side effects, use the Fay
monad to represent this:
unixTime :: Fay Int
unixTime = ffi "new Date().getTime()"
You can only use point free style in FFI functions, foo x = ffi "..." x
is not allowed. #253
You can also use the FFI on the expression level:
add3 :: Int -> Int -> Int -> Int
add3 x y z = x + (ffi "%1 + %2" :: Int -> Int -> Int)
Usually you want to access some JavaScript global it's a good idea to always use global access so that a local fay binding won't interfere:
alert :: String -> Fay ()
alert = ffi "window.alert(%1)"
If you want to access window
(browser) or global
(nodejs) you can do:
log :: String -> Fay ()
log = ffi "(function () { this.console.log(%1); }).call()"
For Google Closure's advanced optimizations you need to use string access to properties:
jQuery :: String -> Fay JQuery
jQuery = ffi "window['jQuery']"
Fay
, like IO
in Haskell, should be used for anything that may include a side effect. ffi "%1 + %2"
is obviously pure. ffi "console.log(%1)"
is not. getWindow = ffi "window"
is a bit more subtle. The window object is always the same, but its properties mutate. The safe bet is to stick with Fay when uncertain and fay-dom
defines it as getWindow :: Fay Global
.
You can serialize to and from records automatically like this:
data Con = Con Double String
printCon :: Fay ()
printCon = print (Con 1 "str") -- Will output { instance : 'Con', slot1 : 1, slot2 : 'str' }
data Rec = Rec { a :: Double, b :: String }
printRec :: Fay ()
printRec = print (Rec { a = 1, b = "str" }) -- Will output { instance : 'Rec', a : 1, b : 'str' }
getRec :: Fay Rec
getRec = ffi "{ instance : 'Rec', a : 1, b : 'str' }" -- Gives you Rec { a = 1, b = "str" }
Fay types are represented as (mostly) JSON, with the exception that we allow atomic values on the top level. Fay functions remain functions when serialized, but conversions are added so they can be called from JS, and vice versa.
Additionally, an instance
property is required for ADTs, to let Fay to deserialize JSON into a Fay value properly.
Atomic values have the same representation in Fay as they do in JS, they serialize to top level values which are not valid JSON strictly speaking:
1 :: Int <-> 1
1 :: Integer <-> 1
3.14 :: Double <-> 3.14
() :: () <-> null
"abc" :: String <-> "abc"
"abc" :: Text <-> "abc"
Note that you cannot use Automatic when serializing [Char]
(String
) since Fay cannot distinguish between a list and a string in the generic case.
"abc" :: Automatic a <-> ["a","b","c"]
Are represented as JS arrays
["apple","banana"] :: [String] <-> ["apple","banana"]
Are also represented as JS arrays
("x",3.14,27) :: (String,Double,Int) <-> ["x",3.14,27]
All constructors are converted to objects with the constructor's fields as properties. An instance field is added so Fay can deserialize the values using the correct constructor.
data Person = Person { name :: String, age :: Int }
Person "Adam" 100 :: Person <-> { "instance": "Person", "name": "Adam", "age" : 100 }
Properties for constructors without field names will be serialized with the properties "slot1"
.."slotN"
.
data Money = Dollar Int | Euro String Int
Dollar 100 :: Money <-> { "instance" : "Dollar", "slot1" : 100 }
Euro "Dutch" 200 :: Money <-> { "instance" : "Euro", "slot1" : "Dutch", "slot2" : 200 }
Here are the rules for serialization. All serialization is based on the type you specify at the ffi declaration, e.g.:
foo :: Int -> String
foo = ffi "JSON.stringify(%1)"
The rules are simple:
- Concrete type, e.g.
String
,Maybe Int
: yes, will be de/serialized. - Polymorphic, e.g.
a
,Maybe a
: will not be touched.
There are two helpers for turning on/off serialization for the above:
-
Ptr Foo
: will not be touched. -
Automatic a
: will attempt to automatically serialize thea
at runtime.
There are two utility types for interfacing with JS APIs:
-
Nullable a
:Null
will be serialized tonull
. -
Defined a
:Undefined
will be serialized toundefined
, and will be ommitted from serialized objects e.g.Foo Undefined
→{"instance":"Foo"}
.
Maybe a
has different semantics so we couldn't use it for these cases. If you want to decode null
to the type Maybe (Maybe a)
there is no way to tell whether the correct value is Nothing
or Just Nothing
so we chose to not do special serialization for Maybe
. When interfacing with JavaScript, this does not matter since its lack of representation for Just Nothing
was the problem!
Please see this comment.
In older versions of Fay there was a Foreign
type class. This didn't actually do anything in itself, it was just used to add a tiny bit of manually enforced type safety. In new code you can just remove Foreign
declarations, it won't impact serialization behavior.