I've noticed from the reserved keyword list in the tutorial that out of the 23 listed keywords, 11-13 (I'm not sure about with and to) are used exclusively in the file header, many of which can appear at most once per file. Put another way, the file header represents a negligble amount of real code volume, yet possesses about half of the language keywords.
Aside: I have read through the Proposal: Module and Package Changes doc. That does clean up some a lot of the syntax, and removes/exchanges out some of the keywords compared to what is in the tutorial, but still has a lot of one-off keywords.
Some thoughts:
requires and provides are not too likely to be desirable as application identifiers, others, like app and package may be useful in specific domains. Note, this concern isn't really about making these keywords specifically available for use, but rather about the complexity to the language. This is a bit like the PURGE BINARY LOGS statement in MySQL: it's something that exists in the language, that adds PURGE and LOGS as keywords for something a niche set of people use under niche circumstances. In Roc's case, it's not a niche set of people involved whole use those keywords, but it may well be a niche set of Roc programmers that remember/understand/intuit the exact syntax rather than copy-pasting the header each time.imports vs requires and exposes vs provides (footnote 1).exposes come before or after imports? Answer: exposes, or the new syntactic equivalent comes first. Similarly, in the platform package case, does provides or requires comes first? Since exposes comes first, and provides sounds like exposes, I'd incorrectly infer that provides comes before requires. I believe the above-linked proposal addresses a lot of this, but not necessarily all of it.exposes contents are specified in something that looks like a list literal. It's not clear when keyword x, y, z or keyword [x, y, z] is favored, for example. I'm inferring that the latter is a better multiline construct.package and packages, this feels a bit too natural-language expressive to me. If we're going to pay out of our keyword budget to get packages, why not spend that same cost to get uses or use, which are unlikely to be good identifiers, and, like from and to, may see more opportunities for syntactic reuse beyond the file header.Footnote 1: Other languages have both package and module concepts that don't really have any cross-language consistent meaning (often the meanings flip-flop across languages), so I'm going to just shrug on that one, since having a not-standardized-across-languages take on those two terms is pretty standard language design practice :wink:
I don't have a single specific counter-proposal, but I do have a few mutually-exclusive themes I've been thinking about:
The old, preproposal syntax already does this somewhat, such as with {pf: ...}. We can take this further, while having fewer keywords:
# The `module` keyword is used for *all* Roc files.
# App is a tag. Outside of this context, it has no special meaning.
# App, Package, Platform, etc are accepted as tags here,
# and thus module accepts a specific tag union.
module Library
{
exposes: [
Request,
Response,
req,
],
imports: {
Json, # special syntactic concession that value-list is optional.
Unicode as Uc,
InternalHttp: [Req, Resp],
Codepoint as Cp: [Codepoint, first],
},
...
}
exposes, imports, etc are just record fields, thus not keywords. The only expenditure out of the keyword budget here is module. The entire header can have as much richness as we need without paying for additional keywords unless they're high-value keywords that are unlikely to be used as variables (as is the case with as, from, etc).
app, package, etc as non-keywordsAlternatively, we could have a syntax like:
# The `module` keyword is used for *all* Roc files.
# `package` is not a keyword, as it doesn't *introduce* a declaration:
# it merely qualifies `module` and is thus either a special
# "ephemeral" identifier (it goes out of scope as soon as declared)
# or is a pseudo-keyword (i.e. only has special meaning when it follows
# the real keyword (in this case `module`).
module package
# `export` is a keyword, and is a counterpart to `import`
# from the linked-doc proposal.
# It may be repeated, and the format tooling is tolerant
# of `export` and `import` lines being in any order,
# but will reorder them to align with whatever order the compiler requires.
# Note: `export` juxtaposes `import` better than `exposes`.
export Request, Response
export req
import Json
import Unicode as Uc
import [Req, Resp] from InternalHttp
import [Codepoint, first] from Codepoint as Cp
The above use of package doesn't follow the terminology that Roc currently uses, and this isn't really a proposal to deviate from the existing Roc nomenclature. However, the proposal is to use a single keyword (such as module) to drive every kind of distinct Roc file, and so module module (or module Module) doesn't really work. module library could perhaps work.
In any case, for what Roc calls a package, I suggest import be used instead of packages, since if a package file cannot do anything aside from import and re-export whole modules, then it hypothetically doesn't need separate keywords (especially keywords which are used just one time each in the header of one or two Roc file types, yet which restrict the names that can be used across all Roc code).
I suspect a similar approach could be applied to use of requires and provides in a platform file, though that'd be a non-concern with the "Embrace Roc types" approach mentioned earlier.
Side note: there is some inconsistency above between export (takes comma-separated tokens) and import (takes something like a list literal in the from form). This inconsistency is intended to have us consider whether and what to bracket in the header. The doc-linked proposal form of import is almost identical to Python imports, except that Python imports are unbracketed:
import stdin, stdout from sys
Do we need to bracket them? Bracketing would enable line wrapping, but I think it'd be unfortunate to see:
import [
Req,
Resp,
] from InternalHttp
The above isn't visually well balanced, and even when not line-wrapped, long lists of symbols to import may require horizontal scrolling to find the actual module being imported from (i.e. harm readability). I believe putting longer, variable-length things to the end of a line helps readability, i.e. put the high-level detail (the module name) before the low level minutiae:
import Codepoint as Cp with [Codepoint, first]
... which can also be more elegantly line-wrapped than the list-first approach:
import Codepoint as Cp with
[
Codepoint,
first,
]
A plus for writing the module name first is that you could get autocompletion on the functions exposed from that module.
Interesting ideas. I remember we did talk about header exclusive keywords one time, there is no need for them to be "global" keywords. I'm not sure how that is currently implemented though.
Hannes Nevalainen said:
A plus for writing the module name first is that you could get autocompletion on the functions exposed from that module.
As someone who cares about tooling a bunch, this is a big deal
The syntax in the module and package changes proposal for import was later revised in Zulip and we're going with:
import Codepoint as Cp exposing [Codepoint, first]
I like that reversal. Out of interest, what is the with keyword presently/planned to be used for in Roc? Aside from being listed as reserved, it doesn't appear to be mentioned in the tutorial.
I found with here.
ah. with seems like it'd be reusable in various places, so feels like a decent keyword to me
I would hope that any keyword only used in the module header would be available as variable names and not reserved. For example, app and platform can only be used in a very specific location. No need to reserve them as a keyword.
I think that will resolve most of the extra keywords
I've been independently thinking whether we should use record syntax for the module headers
I agree that in the current design, they should only be reserved keywords in the header itself, not the rest of the file
however, that doesn't work well for syntax highlighting, especially of code snippets
like in the tutorial, either we always highlight them as keywords or else we never do, because code snippets usually don't have a module header, so we can't know whether we're in the context of the header or not
separately, I think it would be nice to make order optional on these (roc format can reorder them) and also to make some of the header entries themselves optional
and records have all of those properties, so it feels like a natural syntax to choose for them
I also like the idea of curly braces clearly delimiting where the header begins and ends
also we've discussed before how some of the keywords in the header can be interpreted in multiple ways (e.g. is "imports" a verb, as in "this module imports these things," or is it a noun, as in "these are this module's imports") and I think that ambiguity kind of goes away if they're fields; personally I always read { imports: ... } as a noun because record fields are usually not verbs
These are the headers in the Module and Package Changes proposal:
module [Request, Response, req]
module { echo, read } -> [menu]
app [main] requires [Stdout from "https://…"]
platform package [Stdout, Stdin]
requires [main]
provides [mainForHost] to "prebuilt-hosts/"
packages [
Foo, Bar, Baz from "https://…",
Something as Smt from "https://…",
]
package [ParserCore, ParserCsv, ParserStr]
packages [
JsonDecode from "https://…/json/…",
CodePt, Segment from "https://…/unicode/…",
]
However, it was later decided in Zullip to keep the package shorthand syntax, so I guess they should really look like this:
module [Request, Response, req]
module { echo, read } -> [menu]
app [main] requires { pf: "https://…" }
platform package [Stdout, Stdin]
requires [main]
provides [mainForHost] to "prebuilt-hosts/"
packages {
foo: "https://…",
smt: "https://…",
}
package [ParserCore, ParserCsv, ParserStr]
packages {
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
@Richard Feldman Is this how you imagined them using a record-like syntax?
module [Request, Response, req]
module { echo, read } -> [menu]
app [main] {
requires: { pf: "https://…" }
}
platform package [Stdout, Stdin] {
requires: [main],
provides: {
values: [mainForHost]
to: "prebuilt-hosts/",
},
packages: {
foo: "https://…",
smt: "https://…",
}
}
package [ParserCore, ParserCsv, ParserStr] {
packages: {
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
}
I kept the exposed values outside of the record because I really like how compact the module header looks
yeah, that looks good to me! :thumbs_up:
What if the app one looked like this?
app [main] {
pf: "https://…"
}
It could work for package too:
package [ParserCore, ParserCsv, ParserStr] {
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
I'm up for trying it! :thumbs_up:
It doesn't work for platform package but that shouldn't come up that often
Yeah, I think this looks pretty nice:
module [Request, Response, req]
module { echo, read } -> [menu]
app [main] {
pf: "https://…"
}
package [ParserCore, ParserCsv, ParserStr] {
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
platform package [Stdout, Stdin] {
requires: [main],
provides: {
values: [mainForHost]
to: "prebuilt-hosts/",
},
packages: {
foo: "https://…",
smt: "https://…",
}
}
The multi-line list for app, package, and platform package might look a little weird but that should be uncommon
might be fine too
well, the multiline app case is uncommon, not sure about package
yeah probably decently common with package
but let's see if it's a problem in practice
Would you format it like this?
package
[
ParserCore,
ParserCsv,
ParserStr
]
{
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
yeah I think so
These options look weird to me:
package [
ParserCore,
ParserCsv,
ParserStr
] {
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
package [
ParserCore,
ParserCsv,
ParserStr
]
{
json: "https://…/json/…",
unicode: "https://…/unicode/…",
}
yeah I think just indenting all of it would look best
the first way
Do you think we’d ever want to add more metadata to the package header? Like a description maybe
I suppose a doc comment is better for that
but I think I remember package having more fields from some other proposal
Aha! Proposal: Package Versioning
That proposal introduces the previous field:
previous 1.0.4 "abcdef234ad932.tar.br"
hrm yeah
@Agus Zubiaga what about making the exposes list just another record field? I'm a bit rusty in Roc, so I'm not 100% sure these _are_ exposed symbols, and I suspect many beginners would be confused as well. However, using a named field would eliminate the confusion:
module {
exports: [Request, Response, req]
}
package {
exports: [ParserCore, ParserCsv, ParserStr],
# ...
}
@Agus Zubiaga I'd say let's not block on previous. I haven't thought it through all the way, but I'm not confident that putting that in the module header is the best design, so it might end up somewhere else (or the design might change in other ways) when we get around to implementing it.
Kevin Gillette said:
Agus Zubiaga what about making the exposes list just another record field?
I thought about that, but I don’t know how I would fit module params in there.
It feels weird to use a record field for them because they are a pattern (record destructuring)
Whereas the other fields are more like literals, not patterns
@Agus Zubiaga can you clarify the pattern aspect you mentioned? It looks like the recent examples are just simple symbol references. I might not have encountered more complex cases yet.
@Kevin Gillette Sure! In the following example the thing between module and -> is a record destructure pattern:
module { echo, read } -> [menu]
Just like you might find in a lambda, def, or when branch:
\{ echo, read } -> …
Also, in my mind, these aren't real semantic records, but rather just record (or record-like) syntax, and so the rules/expectations don't necessarily need to be the same.
For example, the list of things to expose certainly can't be a real List because the elements will have different types (and since Roc doesn't have a first-class type concept, they couldn't be used as values).
@Agus Zubiaga thanks for the example! So that reads as "echo and read are provided to menu" ?
Yeah, I get that. I just think it’s weird syntactically to have something that looks like a record literal to contain a pattern.
Or maybe I read that backwards, and it's supposed to be "echo and read are provided by main" ?
The RHS of -> is the list of exposed names. You can think of it as if the whole module was a function that takes the params and returns the exposed values and types.
echo and read are available to the whole module, including unexposed defs
Agus Zubiaga said:
Kevin Gillette Sure! In the following example the thing between
moduleand->is a record destructure pattern:module { echo, read } -> [menu]Just like you might find in a lambda, def, or
whenbranch:\{ echo, read } -> …
Crazy idea... Would including a slash make it clearer what this is?
module [Request, Response, req]
# added a slash here
module \{ echo, read } -> [menu]
app {
pf: "https://…"
} -> [main]
package {
json: "https://…/json/…",
unicode: "https://…/unicode/…",
} -> [ParserCore, ParserCsv, ParserStr]
platform package {
requires: [main],
provides: {
values: [mainForHost]
to: "prebuilt-hosts/",
},
packages: {
foo: "https://…",
smt: "https://…",
}
} -> [Stdout, Stdin]
I thought more about this and I think it might only make sense maybe to include a slash on the module level where we have module params.
I also re-ordered the app and package because I feel like the record is like a config being provided and having the exposes stuff after the -> feel like these are the output from the app, package, or platform (from a roc users perspective). So input on the left and output on the right.
Also for a module without any params, I think it would be good to have the syntax be something like this.
# takes an empty record => no params
module \{} -> [Request, Response, req]
I'm not a massive fan of the slash tbh... but the rest of it seems kind of worthwhile. Re-order, include -> for all of the patterns, and taking an empty record for a module with no params.
fwiw, in terms of destructuring, {a, b} seems closer to how exposing/using variables across modules works, more so than [a, b]
Outside of the module header, Lists are ordered unnamed values of a single type, accessed by position, while records are unordered named values, each of arbitrary type, accessed by name.
However, within the module header, Lists hold symbols of mixed type, accessed by name, where order is irrelevant.
Thus, even without considering other changes to the existing syntax, it seems more self-consistent to have:
module { Request, Response, req }
module { echo, read } -> { menu }
That would also permit renaming in all cases using record destructuring:
module { Request, Response, req: doReq }
module { print: echo, read } -> { menu: renderMenu }
Looking beyond the present syntax, it doesn't feel fundamental to me that it be expressed as a destructuring-style pattern for at least a couple reasons:
import and export concepts, and express them as separate fields in a record. Each of these fields could be a list or shorthand record.module
[
listFiles,
open,
read,
write,
seek,
close,
] -> [
walk,
removeAll,
rename,
backup,
]
Records can handle multiple lines gracefully:
module {
params: {
listFiles,
open,
read,
write,
seek,
close,
},
exports: {
walk,
removeAll,
rename,
backup,
}
]
I'm torn. I see how it helps in that case, but I still love the simplicity of the original syntax.
I expect most modules not to have any params because they'd just expose functions around a data structure, no effects. Of the ones that would produce effects, they can just import from the platform directly unless they're in a package.
That last example you provided is realistic for a file-handling package. However, I think other packages won't need as many effects. For instance, of 16 .roc modules in roc-pg, only one needs to produce effects (4); everything else is data structures and functions around them.
So the intersection of modules for which this is better looks small to me, and even for those, is the multiline arrow example that bad?
yeah I definitely think we should try the more concise one first and see how it goes in practice
Last updated: Jun 16 2026 at 16:19 UTC