PureScript の ffi を使ってみる

PureScript から JavaScript のコードを呼ぶ

JS のコードを呼ぶ方法を調べたのでまとめておきます。

Test.pursTest.js という名前でファイルを作ります。(ファイル名が異なる場合はコンパイルエラーになります。)

実例によるPureScriptの第10章 外部関数インタフェースが詳しいので、そっちを読んだ方がわかりやすいかも。

一番簡単な例

module Test where  

import Prelude  

import Effect (Effect)  

foreign import test :: Effect Unit  

"use strict";  
exports.test = function() {  
  console.log("test")  
}  

これで test 関数を使う準備ができました。単純に foreign import <func> :: <type> のような感じですね。

以下のmain関数を実行すると、ちゃんと動作していることがわかります。

module Main where  

import Prelude  

import Effect (Effect)  
import Effect.Console (log)  

import Test (test)  

main :: Effect Unit  
main = do  
  log ""  
  test  

λ spago run  
  
test  

引数を取る関数の場合

さきほどのtest.jsを少しだけ変更して引数を取るようにします。

"use strict";  
exports.test = function(x) {  
  console.log(x)  
}  

module Test where  

import Prelude  

import Effect (Effect)  

foreign import test :: Effect Unit  

ここで PureScript のコードはそのままにしてみましょう。この場合、testには誤った型がついていることになります。実行してみます。

λ spago run  
  
undefined  

ffi を使う時は型宣言に注意ですね。

では、型を正しく修正して再度実行してみます。

module Test where  

import Prelude  

import Effect (Effect)  
import Data.Function.Uncurried  

foreign import test :: forall a. a -> Effect Unit  

module Main where  

import Prelude  

import Effect (Effect)  
import Effect.Console (log)  

import Test (test)  

main :: Effect Unit  
main = do  
  log ""  
  test 0  

実行結果

λ spago run  
  
0  
/Users/gupi/Desktop/ffi/output/Main/index.js:7  
    return Test.test(0)();  
                       ^  

TypeError: Test.test(...) is not a function  
    at Object.__do [as main] (/Users/gupi/Desktop/ffi/output/Main/index.js:7:24)  
    at Object.<anonymous> (/Users/gupi/Desktop/ffi/.spago/run.js:3:29)  
    at Module._compile (internal/modules/cjs/loader.js:759:30)  
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)  
    at Module.load (internal/modules/cjs/loader.js:628:32)  
    at Function.Module._load (internal/modules/cjs/loader.js:555:12)  
    at Function.Module.runMain (internal/modules/cjs/loader.js:822:10)  
    at internal/main/run_main_module.js:17:11  
[error] Running failed; exit code: 1  

値は表示されるものの、よくわからないエラーになりました。生成されたindex.jsを確認します。

λ spago bundle-app  
[info] Installation complete.  
[info] Build succeeded.  
[info] Bundle succeeded and output file to index.js  

// ...省略  
(function($PS) {  
  // Generated by purs version 0.13.5  
  "use strict";  
  $PS["Main"] = $PS["Main"] || {};  
  var exports = $PS["Main"];  
  var Effect_Console = $PS["Effect.Console"];  
  var Test = $PS["Test"];                  
  var main = function __do() {  
      Effect_Console.log("\ud83c\udf5d")();  
      return Test.test(0)();  
  };  
  exports["main"] = main;  
})(PS);  

test 関数の呼び出し時に()が1つ多いですね。

期待する動作を得るためには test.js を以下のように修正します。

"use strict";  

exports.test = function(x) {  
  return function() {  
    console.log(x)  
  }  
}  

λ spago run  
  
0  

2引数関数 (カリー化されていない)

test.js を2引数関数にしましょう。

"use strict";  

exports.test = function(x, y) {  
  return function() {  
    console.log(x)  
    console.log(y)  
  }  
}  

同様に PureScript のコードも修正します。

module Test where  

import Prelude  

import Effect (Effect)  

foreign import test :: forall a b. a -> b -> (Effect Unit)  

module Main where  

import Prelude  

import Effect (Effect)  
import Effect.Console (log)  

import Test (test)  

main :: Effect Unit  
main = do  
  log ""  
  test 0 true  

実行結果

λ spago run  
  
0  
undefined  
/Users/gupi/Desktop/ffi/output/Main/index.js:7  
    return Test.test(0)(true)();  
                             ^  

TypeError: Test.test(...)(...) is not a function  
    at Object.__do [as main] (/Users/gupi/Desktop/ffi/output/Main/index.js:7:30)  
    at Object.<anonymous> (/Users/gupi/Desktop/ffi/.spago/run.js:3:29)  
    at Module._compile (internal/modules/cjs/loader.js:759:30)  
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)  
    at Module.load (internal/modules/cjs/loader.js:628:32)  
    at Function.Module._load (internal/modules/cjs/loader.js:555:12)  
    at Function.Module.runMain (internal/modules/cjs/loader.js:822:10)  
    at internal/main/run_main_module.js:17:11  
[error] Running failed; exit code: 1  

またエラーになりました・・・。これはjsのコードがカリー化されていないためです。

まずは purescript-functions をインストールしましょう。

λ spago install purescript-functions  

次に型を a -> b -> Effect Unit から Fn2 a b (Effect Unit) に変更します。2引数関数なので Fn2を使います。(ライブラリでは Fn10 まで定義されています。)

module Test where  

import Prelude  

import Effect (Effect)  
import Data.Function.Uncurried (Fn2)  

foreign import test :: forall a b. Fn2 a b (Effect Unit)  

次に Main.purs では runFn2 関数で test を呼びます。

module Main where  

import Prelude  

import Effect (Effect)  
import Effect.Console (log)  

import Test (test)  
import Data.Function.Uncurried (runFn2)  

main :: Effect Unit  
main = do  
  log ""  
  runFn2 test 0 true  

実行結果

λ spago run  
  
0  
true  

2引数関数 (カリー化されている)

最後に js の定義がカリー化されている場合のコードを載せておきます。

"use strict";  

exports.test = function(x) {  
  return function(y) {  
    return function() {  
      console.log(x)  
      console.log(y)  
    }  
  }  
}  

module Test where  

import Prelude  

import Effect (Effect)  

foreign import test :: forall a b. a -> b -> (Effect Unit)  

module Main where  

import Prelude  

import Effect (Effect)  
import Effect.Console (log)  

import Test (test)  

main :: Effect Unit  
main = do  
  log ""  
  test 0 true  

λ spago run  
  
0  
true  

この場合 PureScript側はいつも通りの感じで書けます。

参考リソース