Blog
May 10, 2024

A Practical Use Case for Function Capture in Gleam

A common question for Gleam newcomers is, “What is the use of function captures?”

It’s very likely that you’ve already used it if you’ve built a basic Wisp web app. Here’s an example:

pub fn main() {
  let ctx = Context(static_directory: static_directory())

  let handler = handle_request(_, ctx) 

  let assert Ok(_) =
    wisp.mist_handler(handler, "secret_key") 
    |> mist.new
    |> mist.port(8000)
    |> mist.start_http
  process.sleep_forever()
}

pub fn handle_request(req, ctx) {
  // ...
}

In this example, we’re passing handler to the first parameter of wisp.mist_handler.

See how we’re passing ctx to the second argument of handle_request, even though wisp.mist_handler expects a function with a single parameter, which is req.

Again, mist_handler expects this for its first parameter:

fn request_handler(req) {}

And yet, we were able to give it this:

fn request_handler(req, ctx) {}

How were we able to pass a second parameter (ctx) while it only can accept a single parameter (req)?

That’s because of the magic of function captures.

Function captures in Gleam are nothing more than syntactic sugar for partial application in functional programming.

// Writing this
let add_1 = add(_, 1)
// is the same as this
let add_1 = fn(a) { add(a, 1) }

Partial application is just a technique to apply some arguments (not all) to a specific function.

For example, if I gave you this function:

fn add(a, b) {
  a + b
}

And I asked you to convert it into another function that requires only the a argument and always uses 1 for b, how would you do it?

We can do this by defining a new function that calls that function with b=1, like this:

fn add_1(a) {
  // Calling original `add` function here.
  // We are passing b as 1.
  add(a, 1)
}

Now I can use add_1 instead of add, where b is always 1. So, I only need to pass a.

add_1(4) // 5 because a=4 and b=1

Notice anything similar to the first example in this article?

It’s the same case as the handler function for wisp.mist_handler. It only expects a single parameter, yet we were able to send it another parameter: ctx. Let me show you how we were able to do that.

So, let’s apply what we learned in the add_1 example.

The handler function that wisp.mist_handler expects is this:

fn request_handler(req) {}

But we want to pass ctx as the second parameter, like this:

fn request_handler(req, ctx) {}

We need to use partial application to convert fn request_handler(req, ctx) {} to fn request_handler(req) {}.

We can do that by wrapping the original function call with another function that takes a single parameter. (By the original function, I mean request_handler(req, ctx) {})

fn request_handler(req) {
  our_request_handler(req, ctx)
}

Now, if we pass request_handler to wisp.mist_handler, it will work because it expects a single-parameter function, and we are providing exactly that.

Since Gleam is a beautiful, concise language, it provides us with a shorthand for this. So, we can rewrite it like this:

let request_handler = our_request_handler(_, ctx)

And that’s the perfect use case for function captures in Gleam.

Do you have any questions?
Stay up-to-date on the latest articles from Gleaming