PureNix - Nix backend for PureScript

PureNix has hit 1.0 and I think it’s time for a formal announcement!

From the readme:

PureNix is a Nix backend for PureScript.

Sometimes, you find yourself having to write Nix code that’s more complicated than what the language was designed for. PureNix allows you to write that code in a fully-featured, strongly-typed language instead, and then compile to Nix. A typical example is parsing of configuration files like the port of cabal2nix that inspired PureNix.

PureNix has full support for all of PureScript’s features, including data types, type classes, and calling back into Nix using the FFI.

On the organization page for PureNix you will find a number of packages intended to be used with PureNix, including ports of libraries like purescript-prelude.

Code Sample

PureScript source, Main.purs:

module Main where

import Data.A as A
import Data.B as B

greeting :: String
greeting = "Hello, world!"

data Maybe a = Nothing | Just a

fromMaybe :: forall a. a -> Maybe a -> a
fromMaybe a Nothing = a
fromMaybe _ (Just a) = a

foreign import add :: Int -> Int -> Int

foo :: Int
foo = add A.bar B.baz

Nix FFI file, Main.nix:

{ add = a: b: a + b; }

Generated Nix:

let
  module = 
    { "Data.A" = import ../Data.A;
      "Data.B" = import ../Data.B;
    };
  foreign = import ./foreign.nix;
  add = foreign.add;
  Nothing = {__tag = "Nothing";};
  Just = value0: 
    { __tag = "Just";
      __field0 = value0;
    };
  greeting = "Hello, world!";
  fromMaybe = v: v1: 
    let
      __pattern0 = __fail: if v1.__tag == "Nothing" then let a = v; in a else __fail;
      __pattern1 = __fail: if v1.__tag == "Just" then let a = v1.__field0; in a else __fail;
      __patternFail = builtins.throw "Pattern match failure in src/Main.purs at 11:1 - 11:41";
    in
      __pattern0 (__pattern1 __patternFail);
  foo = add module."Data.A".bar module."Data.B".baz;
in
  {inherit greeting Nothing Just fromMaybe add foo;}

More information can be found on the project’s GitHub page.

PureNix is a project by @cdepillabout and myself, we’d be happy to answer any questions either here or on the GitHub!

9 Likes

This is very interesting. The Maybe and Either monads (together with the do notation and the bind operators >>=, >>) are something that I really miss in Nix.

Also very cool logo!

1 Like

Quick Start

Here’s a quick-start guide for playing around with PureNix.

In general, I recommend you take a look at some of our libraries to see exactly how they are packaged, and copy what they do. For instance, purescript-foldable-traversable or purescript-maybe.

Currently, we only have a temporary package set. This post will explain how to get started with using it.

Creating a development shell

The PureNix flake exposes a development shell with tools like PureScript, Spago, and PureNix.

If you have flakes enabled, you can drop into the PureNix development shell with a command like this:

$ nix develop github:purenix-org/purenix#use-purenix

From here, use spago to create your PureNix project:

$ mkdir my-cool-project
$ cd my-cool-project/
$ spago init

This creates a few files. You’ll need to edit some of these files in order to work with PureNix.

Edit files

This section explains exactly what you’ll have to change from spago’s defaults in order to use PureNix.

packages.dhall

This file defines the upstream package set you want to use. It is somewhat similar to a stack.yaml file for Haskell packages.

It currently looks like this:

let upstream =
      https://github.com/purescript/package-sets/releases/download/psc-0.14.4-20211028/packages.dhall sha256:df6486e7fad6dbe724c4e2ee5eac65454843dce1f6e10dc35e0b1a8aa9720b26

in  upstream

This is pointing to the official PureScript package set, but you’ll need to change it point to the PureNix package set:

let upstream =
      https://raw.githubusercontent.com/purenix-org/temp-package-set/e3baac367415f4276f74aa7ad792637ca37399cd/packages.dhall

in  upstream

You may want to use a more recent commit, but be warned that not all packages work in all commits. The package set is currently a work-in-progress. Please open an issue on GitHub - purenix-org/temp-package-set: Temporary package set for purenix if something is not working or you’d like to add one of your own packages. See GitHub - purescript/package-sets: PureScript packages for Spago and Psc-Package for what a proper PureScript package set looks like.

(Note that spago will automatically add the sha256:XXX integrity check the first time you run spago build.)

spago.dhall

This file defines your current package. It is similar to a Cabal file for Haskell projects, or a Cargo.toml file for Rust projects.

It currently looks like the following:

{ name = "my-project"
, dependencies = [ "console", "effect", "prelude", "psci-support" ]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}

You’ll need to make the following changes:

  • The temporary PureNix package set doesn’t include some of these dependencies, so you need to remove them. You can add other packages as long as they are defined in the package set.
  • There isn’t any testing support, so remove the test sources.
  • You need to specify that spago should use the purenix executable as the backend.

The new spago.dhall should look something like this:

{ name = "my-project"
, dependencies = [ "foldable-traversable", "maybe", "prelude" ]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs" ]
, backend = "purenix"
}

src/Main.purs

Now you’re ready to actually write some PureScript code.

Edit the src/Main.purs file to look like this:

module Main where

import Prelude

import Data.Foldable (foldr)
import Data.Maybe (Maybe(..))

myNames :: String
myNames =
  foldr (\name accum -> name <> " " <> accum) "" ["Eelco", "Jon", "Graham"]

Building

You can use spago to compile the code:

$ spago build

This creates the directory output/ with all the built code. For instance, your src/Main.purs module is built as output/Main/default.nix. I recommend you take a quick look at it. The conversion between PureScript and Nix is relatively straight-forward (except for pattern matching and type class dictionaries).

Using the built Nix code

You can use the built Nix code as you’d expect:

$ nix repl
nix-repl> main = import ./output/Main/default.nix
nix-repl> main.myNames
"Eelco Jon Graham "

Setting up a flake.nix

It may be convenient to create a flake.nix for your project. This can make it easy to quickly jump into a dev shell.

A simple flake.nix may look like the following:

{
  description = "my-cool-project";

  inputs.purenix.url = "github:purenix-org/purenix";
  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, flake-utils, purenix }:
    flake-utils.lib.eachDefaultSystem (system: {
      devShell = purenix.devShells.${system}.use-purenix;
    });
}

With this in place, you can enter the dev shell just by running nix develop.

Problems / Questions / PRs

Feel free to open an issue or send a PR on GitHub - purenix-org/purenix: Nix backend for PureScript. Transpile PureScript code to Nix..

The PureNix compiler is currently very usable, but the surrounding package set still needs some work. We’d love to get more people to help out with improving both PureNix and the surrounding ecosystem!

5 Likes