2

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.

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

Excellent explanation; on another repository I had not needed to add the extra function() for my tests to pass, interestingly, though I haven't dived into why that was the case. I fixed it up to meet this convention and it remained working, though, so all is well - and great to know about EffectFn* for next time!
This probably means that your tests don't actually test anything. For example, you may be obtaining the value of type Effect a, but never evaluating it to obtain an a, so the test doesn't crash and you think all is well.

Your Answer

Draft saved
Draft discarded

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.