@everyone our latest nightly comes with some significant changes to our module system including new syntax and features.
App headers have been simplified and now only specify the values/types provided to the platform (such as main
), and the packages they depend on.
app [main] {
pf: platform "https://…",
json: "https://…/json/…",
}
Note: The platform package entry now needs to be marked explicitly.
Interfaces have been renamed to "modules" and their headers only specify the values and types they expose. Their name is now only derived from the file path, so you no longer have to keep them in sync.
module [Request, Response, req]
Finally, package headers have also been streamlined to specify what modules they expose and which other packages they depend on.
package [ParserCore, ParserCsv, ParserStr] {
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
For now, platform
and hosted
headers remain the same, but there are plans to update those as we make more changes to the module system (e.g. Task as a builtin).
Instead of specifying the imported modules in the header, we'll now use the import
keyword.
app [main] { pf: platform "https://…/basic-cli" }
import pf.Stdout
main =
Stdout.line! "Hello world"
If you want to bring a name into your scope, you can use the exposing
keyword:
import pf.Stdout exposing [line]
We now handle shadowing correctly, so you'll get a helpful error message if the name is already in scope.
You can now bring the module into scope under a different name using the as
keyword:
import json.Core as Json
You can use this to resolve conflicts if two packages were to use the same name for a module.
The name alias must not conflict with any other names in scope.
Ingested files are still supported and look like this:
import "friends.txt" as friends : Str
import "logo.png" as logo : List U8
Note: The annotation is currently mandatory, but we'll make that optional soon.
You can now introduce an import or ingested file at a deeper scope than the top one.
query =
import pg.Sql exposing [from, select, where, like, str]
users <- from Db.users
select {
name: <- users.name,
}
|> where (users.name |> like (str "John%"))
This is useful if you don't want to pollute your scope with e.g. DSL functions, or test helpers (they can also appear inside expect
). In the future, this will also allow us to provide non-static module params when those are implemented.
You can now import local modules and ingest files in the REPL!
In reimplementing parts of the module system, many bugs have been fixed and error messages have been improved:
exposing
keyword, and for builtin types that appear in exposing
: #4096exposing
conflicts with something else in scope, we will now report this instead of crashingNot everything is fixed, though. There are still old bugs related to packages and other improvements coming later.
The old syntax for headers and imports should still parse and work correctly, so you don't need to rush to update everything. Your app can still use packages and platforms with the old syntax.
That said, running roc format
should upgrade all your code automatically!
CleanShot-2024-05-01-at-15.51.09.mp4
The newest nightlies come with some warnings if your project uses roc-json. Luke's on vacation so I made a quick patch roc-json release from my updated fork.
Ah, yes. I made a PR but it looks like it hasn’t landed yet. I also made an issue to prevent warnings from (remote) package from showing up when you build your app.
This is awesome!! Thanks for all your work Agus! :star:
Updated json release https://github.com/lukewilliamboswell/roc-json/releases/tag/0.8.0
It was just waiting on the new nightly
Using roc format
to update to the new syntax is so slick :star_struck:
Is the distinction between packages and modules that all modules (perhaps aside from the builtins) must live in a package?
Can a module be used without an import as long as its containing package is in the header? i.e. is pf.Stdout.line
allowed in expressions?
Agus Zubiaga said:
You can now introduce an import or ingested file at a deeper scope than the top one.
query = import pg.Sql exposing [from, select, where, like, str] # ...
In other languages this might be discouraged due to it becoming harder for readers to reason about the dependencies/capabilities solely by looking at the top of a file.
I'm guessing this is less of an issue for Roc because utilizing external code involves two layers (packages and imports), and the outer package layer is in the header anyways?
Packages are made up of modules. Packages can be imported remotely and from any local directory.
Modules can only be imported locally/from a subdirectory.
We just discovered that the formatter in the language server is not properly upgrading imports to the new syntax like the CLI formatter is. This PR will fix that, but in the meantime, if you're working on an app with old syntax, you should first run roc format
through CLI to properly upgrade. The LS formatter should work fine after that.
Love the new syntax. Just have a quick question: how can I have a module that depends on a package (like roc-json
for example)?
You have to add it to your app's header first:
app [main] {
pf: platform "...",
json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.9.0/JI4BuuOuWnD1R3Xcx-F8VrWdj-LM_FfDRB00ekYjIIQ.tar.br"
}
import MyModule
and then you can use it in MyModule.roc
like this:
module [things, you, expose]
import json.Json # We use the `json` package shorthand that we defined in the `app`
Currently, if you want to run roc check
/roc test
, you have to run it on the app
file and it will check everything it imports. If you ran it on the module
file, we'll fail to resolve the "json" package shorthand.
In the future , we plan to fix this by searching for the app automatically, and allow you to specify it through a CLI argument too #6538.
Following your example I have this:
main.roc
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br",
json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.9.0/JI4BuuOuWnD1R3Xcx-F8VrWdj-LM_FfDRB00ekYjIIQ.tar.br",
}
import MyModule
import pf.Stdout
import pf.Stderr
main =
when Str.fromUtf8 (Encode.toBytes {} MyModule.jsonCoder) is
Ok o ->
Stdout.line! o
Err err ->
Stderr.line! (Inspect.toStr err)
MyModule.roc
module [jsonCoder]
import json.Json
jsonCoder = Json.utf8With { fieldNameMapping: SnakeCase, skipMissingProperties: Bool.true }
With this I have two issues:
roc_ls
is using 100% of the CPU and sometimes hangsMyModule
into main.This expression has a type that does not implement the abilities it's expected to:
11│ when Str.fromUtf8 (Encode.toBytes {} MyModule.jsonCoder) is
^^^^^^^^^^^^^^^^^^
The type Json does not fully implement the ability EncoderFormatting.
The roc_ls
issue is known and is caused by the issue I described where the compiler does not know where your app is. It's a bug we've had for a while. I'm planning to fix it, but there are a few other things I'm looking into first.
As per the abilities issue, I hadn't seen this one yet. @JRI98 Can you create an issue for that?
Opened https://github.com/roc-lang/roc/issues/6740
thank you!
Would it be possible, to alias an exposed symbol? For example
import Sql.Session exposing [parse as parseSession]
Last updated: Jul 26 2025 at 12:14 UTC