Purescript on AWS Lambda

AWS Lambda is a service for provisioning code without the need to provision servers. It is a more constrained environment then EC2. JVM, Python, and Node.js are the only available runtimes (as of this writing). But you only pay for the compute cycles you use.

Purescript is a typed, functional programming language that compiles to JavaScript. It resembles Haskell, though there are some semantic differences.

The code for this article is on Github. https://github.com/kofno/BasicLambda

AWS Lambda Primer

AWS Lamba allows you to provision your code as a Lambda Function. Each function is a small, constrained runtime environment. You can choose to execute your code on a JVM, Python, or Node.js runtime. Since Purescript targets JavaScript, we will focus on Node.js.

You may deploy your Lambda Function as a single file, or as a zip file containing a full Node.js module.

Lambda Functions support many triggering mechanisms. SNS topics, S3 events, calls through the API Gateway, cron style scheduling, etc. You can configure your function to execute in response to any of these.

Your code will execute in response to a trigger. By default, AWS Lambda requires your index.js file and executes the exported handler function.

A handler function is a javascript function that accepts two arguments.

The first argument is data. Data is just a JavaScript object. Its contents depends on how you are triggering the Lambda Function. We’ll come back to the data object.

The second argument is the context. The context object contains meta information about the lambda. It also contains succeed and fail callbacks for indicating when the function has completed.

Here is an example of what a simple Lambda function handler might look like:

"use strict";

exports.handler = function(data, context) {
  if (data.key1 && data.key2) {
    context.succeed(data);
  }
  else {
    context.fail("I have no idea what kind of object this is.");
  }
};

This is a Lambda function written in Javascript. Let’s try writing one in Purescript.

Purescript Example

Purescript is a pure functional language with strong types. It shares a great deal in common with Haskell. We will be reviewing a couple features of the language that are pertinent to our work. You can read more about Purescript in the Purescript by Example book.

Eff - the Side Effect Type

Every program needs to perform native side effects to be useful. Native side effects includes writing to the screen or sending and receiving data.

Purescript is a pure functional language. Because of this, it needs a special type to compose side effects. We call this type Eff.

The Eff type signature looks like this:

foreign import Eff :: # ! -> * -> *

Let’s unpack this signature.

The # kind is a rows constructor. Rows are like a records object. The ! kind is for side effects. The first part of signature describes rows of side effects.

The * kind describes a type. The * -> * defines a type constructor. The last part of the Eff signature specifies a type constructor.

So an Eff type is a row of side effects wrapped around another type. Here’s an Eff example from the Purescript book:

main :: Eff (console :: CONSOLE, random :: RANDOM) Unit
main = do
  n <- random
  print n

The type of this function tells us a lot. We know there are side effects. We know that the side effects are writing to the console and generating a random number. We know that the function returns a Unit, which is a PureScript type for no meaningful return value.

We are going to be using native side effects in our Lambda function. Remember, I said that the context object has succeed and fail methods. These methods tell the Lambda function to end. This is a native side effect.

Let’s create a simple context type for Purescript.

foreign import data Context :: *

Here we’ve described our Context as a type. We also will need to define an effect. We’ll call our effect LAMBDA.

foreign import data LAMBDA :: !

We need to call succeed or fail on the context object so the Lambda shuts down. Purescript is a functional language. We can’t call methods on the Context type. Instead, we will call these methods from JavaScript, wrapping them in functions using FFI.

Before we write the functions, let’s write out the function signatures.

foreign import succeed :: forall eff. Context -> String -> Eff (lambda :: LAMBDA | eff) Unit

foreign import fail :: forall eff. Context -> String -> Eff (lambda :: LAMBDA | eff) Unit

Both these functions have the same type signature. They take a Context. Then they take a String, which Lambda logs when it ends the process. And they both have a LAMBDA side effect.

Now we’ll look at the JavaScript implementation of these functions. Since both implementations look the same, we’ll only show succeed.

exports.succeed = function(context) {
  return function(message) {
    return function() {
      context.succeed(message);
    };
  };
};

It may surprise you that succeed is not a two argument function. Instead, each item in our type signature is a nested function. This is because PureScript, like Haskell, curries every function call.

Calling succeed with a Context as an argument returns a function. Calling that function with a message argument returns a zero argument function. Calling the zero argument function executes our side effect action.

Processing Data with PureScript Foreign

We are not introspecting the Lambda context object. Because of that, we can get away with a simple representation; the Context type. The data object is a different story. We need to extract data from that object. For that, we will use the purescript-foreign library.

Javascript allows us to poke around in any object using the dot notation. Purescript requires you to be a bit more upfront about the structure of the data you are expecting. For this example, we will assume a simple Javascript object:

{
  key1: "key1",
  key2: "key2"
}

You might receive this object in a Lambda function through the API Gateway. Before we can use this in our Lambda function, need to convert it to a Purescript type. This type will work fine for us:

data LambdaData = LambdaData { key1 :: String
                             , key2 :: String
                             }

The purescript-foreign library will convert a foreign Javascript object into our new LambdaData type. To do this, we will use the Foreign type, the F type, and the IsForeign type class.

The Foreign type represents any data from an unreliable source. It is like our Context type from before.

The F is an error type that returns from our type class instance (more on that soon). F is actually an alias for an Either type. An Either type represents one of two possible outcomes; a Left and a Right. The Left value is usually reserved for reporting an error. The Right value holds the value if the computation was successful. This is the F definition:

type F = Either ForeignError

The IsForeign type class defines a read function. We will provide our own instance of read for the LambdaData type. This is where we provide the code for converting a foreign object to a Purescript type.

instance lambdaDataIsForeign :: IsForeign LambdaData where
  read value = do
    k1 <- readProp "key1" value
    k2 <- readProp "key2" value
    return $ LambdaData { key1: k1, key2: k2 }

Here we are just reading ‘key1’ and ‘key2’ from the foreign value. If they are both there, then we construct a LambdaData type and return it wrapped in a Right. If they are not there, then our read function will return a Left along with the error message.

Writing the Handler

We are now ready to write our Purescript handler. Here’s the type signature I came up with:

handler :: forall eff. Context -> Foreign -> Eff (lambda :: LAMBDA | eff) Unit

The handler function takes a Context. It then takes in the data, as a Foreign. It performs an action that has a side effect, but no meaningful value. The forall eff and | eff just mean “there may be other side effects, too”. We are explicit about listing LAMBDA as a side effect, because we know we are going to call success or fail.

Here is one possible implementation of this function:

handler c d = do
  process $ readData d
  return unit

  where
    readData :: Foreign -> F LambdaData
    readData = read

    process :: F LambdaData -> Eff (lambda :: LAMBDA | eff) Unit
    process (Left err) = fail c $ show err
    process (Right d)  = succeed c $ show d

One last thing; how can we get the Lambda function to call our handler? Purescript curries all function arguments; we can’t create a function that matches Lambda’s expectations.

We will need to keep our index.js file around to execute the Purescript code. The JavaScript file will now look like this:

"use strict";
var lambda = require('BasicLambda')

exports.handler = function(data, context) {
  lambda.handler(context)(data)();
};

Here we call the Purescript handler from within JavaScript. Remember that PureScript functions are always curried. We call the handler and hand it the context. It returns a function to us, which we call by passing in the data. We get a third function back. It is our side effect. This function takes no arguments when we call it.

Notice that our Purescript module is compatible with node’s implementation of CommonJS. We can require the module, just like any other Javascript module.

Conclusion

I tried to show how to write an AWS Lambda function in Purescript. I kept the functionality basic, so I could focus on compatibility. Future tutorials may show fetching S3 objects or manipulating images using ImageMagic.

The full code for this tutorial is available on Github. Clone the repo and you should be able to build and deploy the Lambda yourself. If you deploy the Lambda yourself, play with different test data. See how the Lambda reports errors or success.