Hello External World

In this section, starting from the sacred hello world, we'll see how the development cycle in Neut proceeds.

What You'll Learn Here

We'll explore how to use modules in Neut. More Specifically:

  • How to create a module
  • How to build a module
  • How to execute a module
  • How to add dependencies
  • How to publish your module

Creating a Module

Initialization

Run the following command:

neut create sample

This command creates a new directory ./sample/ and files inside the directory. This directory is an example of a module in Neut. A module in Neut is a directory that contains module.ens.

The command create creates a sample project that performs "hello world". This module can be built and executed by running the following commands:

cd ./sample
neut build sample --execute # => "Hello, world!"

Let's see what a module in Neut is like.

Structure

The content should be something like the following:

sample/
├── cache/
│  └── ...
├── source/
│  └── sample.nt
└── module.ens

The directory cache is where object files (binary files) and dependencies are put in. You won't have to go into the directory for daily use.

The directory source is where we put source files.

The file module.ens contains meta information about this project, such as dependencies.

You can change the locations of special directories such as build or source by using module.ens. See Modules for more information.

module.ens

The content of module.ens should be something like the below:

{
  target {
    sample {
      main "sample.nt",
    },
  },
  dependency {
    core {
      digest "(base64url-encoded checksum)",
      mirror [
        "https://github.com/.../X-Y-Z.tar.zst",
      ],
      enable-preset true,
    },
  },
}

target specifies the name and the main file of the resulting executables. In the case above, neut build will create an executable file sample by compiling sources using the main function in sample.nt as the entry point.

dependency specifies external dependencies. Since our running example doesn't do much, the only dependency is core, which is the same as "prelude" in other languages.

The digest is the base64url-encoded checksum of the tarball.

The mirror is a list of URLs of the tarball.

The enable-preset makes the core library behave like Prelude in Haskell. That is, when enable-preset is true, specified names in the dependency are automatically imported into every file in our module.

Source files

Let's see the content of source/sample.nt:

// sample.nt

define main(): unit {
  print("Hello, world!\n") // `print` is defined in `core`
}

The above code defines a function main that returns a value of type unit. This function prints "Hello, world!\n".

Here, the unit is an ADT that contains only one value Unit. The explicit definition of unit is as follows:

data unit {
| Unit
}

// The Haskell equivalent of the above is (ignoring variable naming conventions):
//   data unit =
//     Unit

Let's try editing the code as follows:

// sample.nt
import {
  core.text.io {print-int},
}

define main(): unit {
  print-int(42) // `print-int` is also defined in `core`
}

Then, build the project:

neut build sample --execute # => 42

You can also obtain the resulting binary:

neut build sample --install ./bin # creates a directory `bin` and put the resulting binary there
./bin/sample # => 42

Defining a function

Of course, you can define a function:

// sample.nt
import {
  core.text.io {print-int},
}

define get-int(): int {
  42
}

define main(): unit {
  print-int(get-int())
}

A function can take arguments. Let's rewrite sample.nt into the below:

// sample.nt
import {
  core.text.io {print-int},
}

define increment(x: int): int {
  add-int(x, 1)
}

define my-add(x: int, y: int): int {
  add-int(x, y)
}

define main(): unit {
  print-int(my-add(10, increment(10))) // # => 21
}

Top-level items like define are called statements. You'll see more in the next section.

As in F#, statements in Neut are order-sensitive. If you were to define main before my-add, the code won't compile. For forward references, you'll have to explicitly declare names beforehand using a statement called nominal, which we'll see in the next section.

Publishing Your Module

Let's publish our module so others can use the functions my-add and increment.

You can create an archive of the current module using neut archive:

neut archive 0-1
ls ./archive # => 0-1.tar.zst

neut archive creates the directory archive at the root of your module. This command also creates an archive for the current module.

The name of a module archive must be something like 0-1, 2-3-1, 1-2-3-4-5-6.

The compiler interprets the names of archives as semantic versions. For example, if you create an archive 1-2-3 and then 1-2-4, the 1-2-4 is treated as a newer compatible version of 1-2-3.

This tarball can be controlled with your version control system like Git and pushed to the remote repository, as usual:

# the usual git thing

pwd # => path/to/sample/

git init

git commit --allow-empty -m "initial commit"
echo "build" > .gitignore
git add .gitignore archive/ module.ens source/
git commit -m "whatever"

git remote add origin git@github.com:YOUR_NAME/YOUR_REPO_NAME.git
git push origin main

This tarball can be used as a dependency, as described in the next section.

Adding Another Module to Your Module

neut get can be used to add external dependencies:

# create a new module
pwd # => ~/Desktop (for example)
neut create new-item
cd new-item

# ↓ add the previous module to our `new-item`
neut get some-name https://github.com/YOUR_NAME/YOUR_REPO_NAME/raw/main/archive/0-1.tar.zst

# you can try the following command for example:
neut get some-name https://github.com/vekatze/neut-sample/raw/main/archive/0-1.tar.zst

The command neut get fetches the tarball from the specified URL and adds it to the current module. The module can then be used as some-name in your module.

The information of the newly-added module is saved to module.ens:

{
  target {
    new-item {
      main "new-item.nt",
    },
  },
  dependency {
    core { .. },
    // ↓ HERE
    some-name {
      digest "..",
      mirror [
        "https://github.com/YOUR_NAME/YOUR_REPO_NAME/raw/main/archive/0-1.tar.zst",
      ],
    },
  },
}

The "real" name of an archive is the digest of the tarball. You define an alias of the module for your convenience.

Using Dependencies

These dependencies can then be used in your code:

// new-item.nt

import {
  core.text.io {print-int},
  some-name.sample {my-add},
}

define main(): unit {
  print-int(my-add(10, 11)) // ← using `my-add`
}

Let's focus on import. This statement specifies the files we want to use in dependencies.

import consists of lines like the one below:

some-name.sample {my-add}

The first component of such a line (some-name) is our alias of the dependency.

What follows (sample) is the relative path to the file from the source directory of the dependency module. Here, you don't have to write the file extension .nt.

Like {my-add} in the example above, every bullet item of an import can optionally have a list of names. Names in these lists are made available after import, as in the example above.

Suppose you didn't write {my-add}. In this case, you can use the fully-qualified form of my-add:

// new-item.nt

import {
  core.text.io {print-int},
  some-name.sample, // removed `{my-add}`
}

define main(): unit {
  // ↓ using the fully-qualified form of `my-add`
  print-int(some-name.sample.my-add(10, 11))
}

So far, we have used the file (source-directory)/sample.nt in the dependency. What if the file we want to import isn't at the root of the source directory?

Suppose the dependency some-name contained a file source/entity/item.nt. In this case, the file item.nt can be imported from new-item.nt as follows:

import {
  some-name.entity.item,
}

We only have to add .entity to specify the path to the file. No surprises.

Importing Files in the Current Module

We now know how to import files in dependencies. Using the same idea, we can also (of course) import files in the current module.

Let's create a new file new-item/source/foo/greet.nt with the following content:

// foo/greet.nt

define yo(): unit {
  print("Yo")
}

This file can then be used from new-item/source/new-item.nt as follows:

// new-item.nt

import {
  this.foo.greet {yo},
}

define main(): unit {
  yo()
}

That is, the name of the current module is always this.

import can import multiple files and multiple names at the same time. For example, the following is a valid use of import:

import {
  this.foo.greet {yo},
  some-name.entity.item {add},
}

Prefixed Import

We can also use so-called qualified imports as in Haskell. Let's remember the example of fully-qualified names:

// new-item.nt

import {
  core.text.io {print-int},
  some-name.sample, // removed `{my-add}`
}

define main(): unit {
  // ↓ using the fully-qualified form of `my-add`
  print-int(some-name.sample.my-add(10, 11))
}

We'll rewrite this example into a "prefixed" form. Firstly, edit the module.ens as follows:

{
  target {..},
  prefix {                  //
    S "some-name.sample",   // ← alias: S -> some-name.sample
  },                        //
  dependency {..},
}

By registering aliases of files in module.ens, you'll be able to use prefixes as aliases for specified files.

We can now rewrite the new-item.nt as follows:

// new-item.nt

import {
  S, // == some-name.sample
  core.text.io {print-int},
}

define main(): unit {
  print-int(S.my-add(10, 11))
}

Unlike Haskell, these prefixes are defined per module, not per file. The prefixes of a file must be consistent inside a module.

What You've Learned Here

  • Use neut create MODULE_NAME to create a module
  • Use neut build TARGET_NAME to build modules
  • Use neut build TARGET_NAME --execute to execute modules
  • Use neut get to add external dependencies
  • Use neut archive and push it to publish modules