Search code examples
javascriptpurescript

PureScript - AFF require(...).main is not a function


Consider the following code example, taken directly from the handbook chapter on Asynchronous Effects.

-- https://book.purescript.org/chapter9.html
module Main where 

import Prelude
import Data.Either (Either(..))
import Effect.Aff (Aff, attempt, message)
import Effect.Class.Console (log)
import Node.Encoding (Encoding(..))
import Node.FS.Aff (readTextFile, writeTextFile)
import Node.Path (FilePath)

copyFile :: FilePath -> FilePath -> Aff Unit
copyFile file1 file2 = do
  my_data <- readTextFile UTF8 file1
  writeTextFile UTF8 file2 my_data

main :: Aff Unit
main = do
  result <- attempt $ copyFile "file1.txt" "file2.txt"
  case result of
    Left e -> log $ "There was a problem with copyFile: " <> message e
    _ -> pure unit

However, when this application is run with Spago Run, it produces the following puzzling error, which seems to resemble a JavaScript error instead of a PureScript error.

require(...).main is not a function

The questions would be a follows:

  1. In what way must this code be changed to generate the desired effect (the copying of the files)
  2. What oversight or configuration error would be the cause of the above error?

Solution

  • Short answer: main has to be an Effect, not an Aff.


    Your main is an Aff, and the error message is absolutely correct: Aff is indeed not a function. It's a non-trivial data structure that represents an async computation, and it cannot be trivially "started" from JavaScript without going into way too much PureScript internals.

    Effect, on the other hand, is modeled as a parameterless function on the JavaScript side. For example, something like this:

    f :: Effect Int
    f = pure 42
    

    Gets compiled to JavaScript like this:

    const f = () => 42
    

    This answer has a somewhat longer discussion of this fact with examples.

    So, if you give your main the type Effect Unit, it would be possible to trivially invoke it from JavaScript like this:

    require("output/Main/index.js").main()
    

    Just calling a function with no parameters. Which is exactly what the runtime is trying to do.


    But what if you need to do async stuff in main?

    You can start an Aff async computation via launchAff_, which itself has type Effect Unit, but takes an Aff computation as a parameter and starts it asynchronously:

    main :: Effect Unit
    main = launchAff_ do
      result <- attempt $ copyFile "file1.txt" "file2.txt"
      case result of
        Left e -> log $ "There was a problem with copyFile: " <> message e
        _ -> pure unit
    

    This way, the main function itself will call launchAff_ and terminate immediately, but the launched Aff computation will continue, and the program won't exit until the last Aff computation stops.