Gren 25S: Easier interop, concurrent tasks and zero-install packages

Published: 2025-07-21

Gren is a pure functional programmming language that aims to be easy to learn and to reason about, while remaining powerful and portable enough for real-world use.

A new version of the Gren programming language is now available! There are no syntax changes in this release, but there are still major features to talk about. Task ports simplify interop with JavaScript. Tasks now run concurrently. Packages can now easily be committed to version control, and the compiler now let you run helper programs defined in packages.

What do we mean by 25S?

Twice a year we release backward incompatible changes of the compiler and the core packages, all of which have their own version number.

25S, or the 2025 Summer release, consists of the following software:

Task ports

Ports is the mechanism we use to send values between JavaScript and Gren without sacrificing Gren's guarantee of immutability and managed side effects.

One limitation of ports is that you can only define Cmd (sending a message from Gren to JavaScript) and Sub (JavaScript to Gren) ports. If you wanted to send a value to JavaScript and immediately get a response, you'd have to model that with two ports.

As an example, if you wanted to use JavaScript to store a value in a database and get the ID of that saved value in return, you'd start by defining two ports: one for storing the value, and one for receiving the result:

port saveToDB : String -> Cmd msg
port valueWasSaved : (String -> msg) -> Sub msg

On the JavaScript side, you'd setup these ports as follows:

const { Gren } = require('./app.js');
const db = require("db");

const app = Gren.Main.init({});

app.ports.saveToDB.subscribe((value) => {
  db.save(value, (err, id) => {
    if (err !== null) {
      app.ports.valueWasSaved.send(id);
    }
  })
})

The above code will work, but has two problems. First, when an ID is received we don't know which command it is a response to. Second, we're currently just ignoring errors. Both of these problems are solvable, but the best solution will depend on your specific use case.

In Gren 25S, we've made this pattern a bit easier by allowing you to define task ports.

Defining the port itself is simple enough:

port saveToDB : String -> Task Json.Decode.Value Int

In JavaScript you can define the task port implementation like so:

const { Gren } = require('./app.js');

Gren.Main.init({
  taskPorts: {
    myTaskPort: async function(str) {
      return str.length;
    }
  }
})

Task ports must be implemented as async functions, or functions returning promises on the JS side. If anything goes wrong, like an exception being thrown, it will be represented as a failed task in Gren, with the exception represented as a JSON value.

Task ports reduces the amount of code you need to write, especially considering that you don't need The Elm Architecture to handle the Sub-based port. It also solves both of the problems we mentioned earlier.

Concurrent tasks

Task is what we use to perform managed side effects, like performing a HTTP call.

If you wanted to perform multiple tasks, you were limited to either perform these sequentially (one after another) or turn them into commands that could be run concurrently using Cmd.batch. This later option required you to deal with the responses using The Elm Architecture, which can be a lot of work in certain cases.

With this release, tasks defined using Task.map2, Task.map3 or Task.andMap will run concurrently without the use of Cmd.batch. There's also a Task.concurrent function for executing a whole array of tasks concurrently. Task.sequence is still available for when you want to perform tasks one after another, but can also be combined with Task.concurrent to reduce the amount of concurrency happening at any given time.

Do note that concurrency is not paralellism. Concurrency allows you to wait for multiple things at the same time, while paralellism allows you to do multiple things at the same time. Using Task.map2 with two HTTP requests will now be faster, but using Task.map2 to compute two separate things will have the same performance as before.

This feature was contributed by Andrew MacMurray.

Zero-install package management

When compiling projects, the compiler will now store dependencies as gzipped files in a gren_packages folder at the root of your project. We recommend that you commit this folder to version control, as it will allow your project to compile even if you don't have network access. It will also mean that you don't have to download dependencies unless you're adding them to your project for the first time.

This feature was part of a rewrite of the package manager. It is now written in Gren itself, and some of the underlying functionality is available in the compiler-node package.

Gren run

As a convenience, there is now a command to compile and execute an application:

gren run Main

You can use this as a simple way to run your test suite:

gren run Tests

That might not sound like much, but if you add a --package flag, you'll be able to execute helper applications defined in packages, almost like using npx in the Node.js ecosystem.

This allows you to do things like define initializers for your package:

gren run --package=xyz/cool-framework Setup

And you do not need to be in a Gren project to run this!

Misc. changes

gren path is a new command that prints paths that are important during compilation: the path to the current project, the backend binary being used and the cache folder.

In 24W we added a command, gren make-static, to allow you to create static executables. This command relied on some JavaScript dependencies used through ports, but since you cannot create static executables when ports are involved, this meant that the compiler itself couldn't use this functionality. We've now removed this functionality from the compiler itself, and instead published the gren-make-static package to NPM that provides the same functionality. In the future, we might ship static executable builds of the compiler through nix.

While adding support for concurrent tasks, Andrew MacMurray also found a way to optimize the execution of tasks and commands. You can expect better performance when running a large number of tasks and commands.

You can now also unwrap Cmd from a Task x (Cmd a) type using Task.executeCmd. This makes it possible to end a Task chain with a Cmd, giving you a limited way to combine such effects.

Mapping functions over more than three values (map4, map5, etc.) have been removed from most modules, with an exception made for the JSON modules. If you ever want to map more than three values at a time, we've added andMap to enable that.

There's a new module called Stream.Log that ignores any error that occurs when sending a value to the Stream. In general, we recommend that you only use this module for logging to the stdout or stderr streams, and use the regular Stream module for most other cases where errors are more likely to occur.

Joey Bright has made several improvements to the Crypto API in response to user feedback. There are no new features, but the types are more correct.

You can now listen for SIGINT and SIGTERM signals in applications targetting the node platform. You can also listen for the event where the JS event loop runs out of work.

Filesystem errors now carries the relevant Path, which tends to be useful for error reporting or debugging.

What's next

With Gren 25S out the door, our attention shifts to 25W wich we aim to ship in December. The goal for that release is to rewrite the language parser in Gren itself, re-introduce gren format and finish the centralized package registry.

With the parser rewrite, the AST will be made available in compiler-node. Formatting will be available as a AST -> String function, which will also open the door for code generation.

Thank you, contributors!

This release was made through contributions from the following people:

Support the project

You can support Gren financially by sponsoring us at ko-fi or by buying merch from the shop. The profits allow members of the core team to take a day off from their day job and spend it developing Gren instead.

Being an open-source project, there are also other ways to contribute. Spread the word. Report bugs. Contribute new features. In any case, you likely want to join our Discord first.

If you want to keep tabs on the development of Gren, follow us on Mastodon or subscribe to the language creator's Youtube channel.