JavaScript-бекенд GHC поддерживает собственную соглашение о вызовах для JavaScript-специфичных внешних импортов. Поддерживаются любые невызванные функции, включая имена функций. Обычно JavaScript-внешние импорты пишутся как невызванная JavaScript стрелочная функция, но также поддерживаются function анонимные функции.
Обращаясь к строке импорта как к невызванной функции, можно включать произвольный JavaScript в импорт, поэтому простой пример может выглядеть так:
foreign import javascript "((x,y) => { return x + y; })"
js_add :: Int -> Int -> Int
14.1. Типы JavaScript FFI
Некоторые типы могут быть использованы напрямую в сигнатурах типов внешних экспортов без преобразования в JSVal. Мы видели в первом примере, что Int является одним из таких типов.
Существует ряд поддерживаемых типов, которые могут передаваться непосредственно таким образом и действуют как примитивы в JavaScript RTS GHC. Это по сравнению со структурами данных, реализованными в Haskell, такими как String — будучи списком, у него нет примитивной реализации на JavaScript, и он не эквивалентен JavaScript-строке.
Следующие типы поддерживаются таким образом:
-
Int, включаяInt32и другие численное значения с фиксированной длиной. -
Int64, и другие 64-битные числа передаются как две переменные в функцию, где первая включает знак и старшие биты. BoolCharAnyByteArray#-
DoubleиFloat -
MVar#, и другие объекты RTS. - Неупакованные кортежи (например,
(# a, b #)) могут появляться в типе возвращаемого значения и создаются в JavaScript с помощью макросов, таких какRETURN_UBX_TUP2(x, y).
Как и в C FFI, типы в JavaScript FFI не могут быть проверены на соответствие внешнему коду, поэтому следующий пример будет успешно скомпилирован — несмотря на то, что 5 не является допустимым JavaScript-значением для типа Haskell Bool.
foreign import javascript "((x) => { return 5; })"
type_error :: Bool -> Bool
14.1.1. JSVal
JavaScript-бекенд имеет понятие нетипизированного «простого» JavaScript-значения под видом типа JSVal. Значения с этим типом в основном непрозрачны для кода Haskell: вы можете представить себе JSVal как тип данных, конструкторы данных которого не экспонируются. Его основное применение заключается в передаче непрозрачных JavaScript-значений от одного вызова FFI к другому.
Тем не менее, модуль GHC.JS.Prim из base содержит функции для работы с внешними JSVal объектами. В настоящее время он предоставляет следующие преобразования:
-
Int<->JSVal(toJSInt,fromJSInt) -
String<->JSVal(toJSString,fromJSString) -
[JSVal]<->JSVal(toJSArray,fromJSArray)
Он также содержит функции для работы с объектами:
-
jsNull :: JSVal— JavaScriptnull -
isNull :: JSVal -> Bool— проверка на JavaScriptnull -
isUndefined :: JSVal -> Bool— проверка на JavaScriptundefined -
getProp :: JSVal -> String -> JSVal— доступ к полям объектов
14.1.2. JavaScript-обработчики событий
Модель выполнения JavaScript основана на функциях обратного вызова, и JavaScript-бекенд GHC реализует их как тип для поддержки полезных программ браузера и программ, взаимодействующих с JavaScript-библиотеками.
Модуль GHC.JS.Foreign.Callback в base определяет тип Callback a, а также несколько функций для построения обработчиков событий из функций Haskell с до трех JSVal аргументами. В отличие от обычной функции, функция Callback передается в FFI как обычная JavaScript-функция, что позволяет нам вызывать эти функции из JavaScript:
foreign import javascript "((f) => { f('Example!'); })"
callback_example :: Callback (JSVal -> IO ()) -> IO ()
printJSValAsString :: JSVal -> IO ()
printJSValAsString = putStrLn . fromJSString
main :: IO ()
main = do
printJS <- syncCallback1 ThrowWouldBlock printJSValAsString
callback_example printJS
releaseCallback printJS
Этот пример вызовет нашу функцию printJSValAsString, через JavaScript, со строкой JavaScript Example! в качестве аргумента. В последней строке освобождается память обратного вызова. Так как Haskell JS-временному выполнению не известно, ссылается ли функция на код JavaScript, память необходимо вручную освобождать, когда она больше не нужна.
В первой строке main, мы видим, где Callback фактически создается, функцией syncCallback1. Функция syncCallback имеет версии до трех, включая версию с нулевым аргументом без суффикса. Для использования обратных вызовов с более чем тремя данными рекомендуется упаковывать данные в JavaScript-объекты или массивы по мере необходимости.
Существует три категории функций, которые создают обработчики событий, с показанными здесь сигнатурами типов для аричности 1 для примера:
-
syncCallback1 :: OnBlocked -> (JSVal -> IO ()) -> IO (Callback (JSVal -> IO ())): Синхронные обработчики событий, которые не возвращают значение. Они принимают дополнительныйdata OnBlocked = ThrowWouldBlock | ContinueAsyncаргумент для использования в случае, если поток блокируется, например, наMVarтранзакции. -
syncCallback1' :: (JSVal -> IO JSVal) -> IO (Callback (JSVal -> IO JSVal)): Синхронные обработчики событий, которые возвращают значение. Из-за возвращаемого значения нет возможности продолжить асинхронно, поэтому не принимается аргументOnBlocked. -
asyncCallback1 :: (JSVal -> IO ()) -> IO (Callback (JSVal -> IO ())): Асинхронные обработчики событий, которые немедленно запускаются в новом потоке. Не может вернуть значение.
Проверка, что переданные аргументы соответствуют обработчику событий, не производится, поэтому следующий пример компилируется и правильно выводит 10, несмотря на то, что аргумент передается как Int в Callback, которая принимает JSVal.
foreign import javascript "((f,x) => { return f(x); })"
apply_int :: Callback (JSVal -> IO JSVal) -> Int -> IO Int
main :: IO ()
main = do
add3 <- syncCallback1' (return . (+3))
print =<< apply_int add3 7
releaseCallback add3
14.1.3. Обработчики событий как внешние экспорты
Обработчики событий JavaScript позволяют выполнять своего рода экспорт FFI через импорт FFI. Для этого устанавливается глобальная JavaScript-переменная, которую затем можно вызывать из сценариев, которые обращаются к обычным JavaScript-функциям, таким как интерактивные HTML-элементы. Это будет выглядеть так:
foreign import javascript "((f) => { globalF = f })"
setF :: Callback (JSVal -> IO ()) -> IO ()
main :: IO ()
main = do
log <- syncCallback1 ThrowWouldBlock (print . fromJSString)
setF log
-- don't releaseCallback log
<button onClick="globalF('Button pressed!")>Example</button>
Мы должны убедиться, что не используем releaseCallback для любых функций, которые должны быть доступны в HTML, так как мы хотим, чтобы эти функции хранились в памяти постоянно.