Search code examples
purescript

Why does this function application generate a runtime error in purescript?


I have the following PureScript snippets; note parseXMLFromString is partially applied:

parseXMLFromString ∷ String → DOMParser → Effect Document
parseXMLFromString s d =
  parseFromString "application/xml" s d

parseNoteDoc :: DOMParser -> Effect Document
parseNoteDoc = parseXMLFromString TD.noteXml

note <- parseNoteDoc domParser

The following code is generated:

// Generated by purs version 0.12.4
"use strict";
var Effect_Console = require("../Effect.Console/index.js");
var Test_Data = require("../Test.Data/index.js");
var Web_DOM_DOMParser = require("../Web.DOM.DOMParser/index.js");
var parseNoteDoc = Web_DOM_DOMParser.parseXMLFromString(Test_Data.noteXml);
var main = function __do() {
    var v = Web_DOM_DOMParser.makeDOMParser();
    var v1 = parseNoteDoc(v)();
    return Effect_Console.log("TODO: You should add some tests.")();
};
module.exports = {
    parseNoteDoc: parseNoteDoc,
    main: main
};

The line var v1 = parseNoteDoc(v)(); gives the error TypeError: parseNoteDoc(...) is not a function.

I'm not sure where the extra () is coming from on parseNoteDoc but that is the issue. When I manually remove the () in the generated source, it works works as expected.

Update: Added the code to reproduce this on this branch. After the usual formalities, npm run testbrowser and open dist/index.html in a browser.


Solution

  • TL;DR: your FFI code is incorrect, you need to add an extra function().


    Longer explanation:

    The extra empty parens come from Effect.

    This is how effectful computations are modeled in PureScript: an effectful computation is not a value, but a "promise" of a value that you can evaluate and get the value as a result. A "promise" of a value may be modeled as a function that returns a value, and this is exactly how it's modeled in PureScript.

    For example, this:

    a :: Effect Unit
    

    is compiled to JavaScript as:

    function a() { return {}; }
    

    and similarly, this:

    f :: String -> Effect Unit
    

    is compiled to JavaScript as:

    function f(s) { return function() { return {}; } }
    

    So it takes a string as a parameter, and then returns Effect Unit, which is itself a parameterless function in JS.

    In your FFI module, however, you are defining parseFromString as:

    exports.parseFromString = function (documentType) {
      return function (sourceString) {
        return function (domParser) {
          return domParser.parseFromString(sourceString, documentType);
        };
      };
    };
    

    Which would be equivalent to parseFromString :: String -> String -> DOMParser -> Document - i.e. it takes three parameters, one by one, and returns a parsed document.

    But on the PureScript side you're defining it as parseFromString :: String -> String -> DOMParser -> Effect Document - which means that it should take three parameters, one by one, and then return an Effect Document - which should be, as described above, a parameterless function. And it is exactly this extra parameterless call that fails when you try to evaluate that Effect Unit, which in reality is not an Effect at all, but a Document.

    So, in order to fix your FFI, you just need to insert an extra parameterless function, which will model the returned Effect:

    exports.parseFromString = function (documentType) {
      return function (sourceString) {
        return function (domParser) {
          return function() {
            return domParser.parseFromString(sourceString, documentType);
          }
        };
      };
    };
    

    (it is interesting to note that makeDOMParser :: Effect DOMParser is correctly modeled in your FFI module as a parameterless function)


    But there is a better way

    These pyramids of nested functions in JS do look quite ugly, you have to agree. So it's no surprise that there is an app for that - EffectFn1, runEffectFn1, and friends. These are wrappers that "translate" JavaScript-style functions (i.e. taking all parameters at once) into PureScript-style curried effectful functions (i.e. taking parameters one by one and returning effects).

    You can declare your JS side as a normal JS function, then import it into PureScript as EffectFnX, and call it using runEffectFnX where needed:

    // JavaScript:
    exports.parseFromString = function (documentType, sourceString, domParser) {
      return domParser.parseFromString(sourceString, documentType);
    };
    
    -- PureScript:
    foreign import parseFromString ∷ EffectFn3 String String DOMParser Document
    
    parseHTMLFromString ∷ String → DOMParser → Effect Document
    parseHTMLFromString s d =
      runEffectFn3 parseFromString "text/html" s d
    

    P.S. People who purchased EffectFn1 also liked Fn1 and friends - same thing, but for pure (non-effectful) functions.