Skip to content

Foreign function interface

jpaugh edited this page Oct 2, 2014 · 18 revisions

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']"

The Fay monad

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.

Records

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

Serialization Format

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

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

Lists

Are represented as JS arrays

["apple","banana"] :: [String] <-> ["apple","banana"]

Tuples

Are also represented as JS arrays

("x",3.14,27) :: (String,Double,Int) <-> ["x",3.14,27]

ADTs

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 }

Serialization

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:

  1. Concrete type, e.g. String, Maybe Int: yes, will be de/serialized.
  2. Polymorphic, e.g. a, Maybe a: will not be touched.

There are two helpers for turning on/off serialization for the above:

  1. Ptr Foo: will not be touched.
  2. Automatic a: will attempt to automatically serialize the a at runtime.

There are two utility types for interfacing with JS APIs:

  • Nullable a: Null will be serialized to null.
  • Defined a: Undefined will be serialized to undefined, 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!

Fay<->Server communication

Please see this comment.

Foreign

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.