Stream: ideas

Topic: Fewer/different keywords in the file header


view this post on Zulip Kevin Gillette (Jan 02 2024 at 05:41):

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:

  1. One-off keywords don't necessarily bring a lot of value. While some, like 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.
  2. Some of these keywords are fairly co-redundant. Roc may ascribe them specific meaning, but in a non-technical context, they may be interchangeable. Particularly this is the case with imports vs requires and exposes vs provides (footnote 1).
  3. Some of the syntax itself isn't necessarily universally intuitive. Should 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.
  4. Mixed use of keywords vs expression-style Roc syntax: in the above-linked proposal, the 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.
  5. Singular and plural keyword pairs: having both 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:

Embrace Roc types

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).

Treat app, package, etc as non-keywords

Alternatively, 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,
  ]

view this post on Zulip Hannes Nevalainen (Jan 02 2024 at 05:56):

A plus for writing the module name first is that you could get autocompletion on the functions exposed from that module.

view this post on Zulip Anton (Jan 02 2024 at 09:27):

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.

view this post on Zulip Eli Dowling (Jan 02 2024 at 11:01):

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

view this post on Zulip Agus Zubiaga (Jan 02 2024 at 14:53):

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]

view this post on Zulip Kevin Gillette (Jan 02 2024 at 16:05):

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.

view this post on Zulip Anton (Jan 02 2024 at 16:39):

I found with here.

view this post on Zulip Kevin Gillette (Jan 02 2024 at 17:05):

ah. with seems like it'd be reusable in various places, so feels like a decent keyword to me

view this post on Zulip Brendan Hansknecht (Jan 02 2024 at 17:15):

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

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:40):

I've been independently thinking whether we should use record syntax for the module headers

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:41):

I agree that in the current design, they should only be reserved keywords in the header itself, not the rest of the file

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:41):

however, that doesn't work well for syntax highlighting, especially of code snippets

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:42):

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

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:43):

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

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:43):

and records have all of those properties, so it feels like a natural syntax to choose for them

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:43):

I also like the idea of curly braces clearly delimiting where the header begins and ends

view this post on Zulip Richard Feldman (Jan 11 2024 at 02:45):

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

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:11):

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/…",
    }

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:17):

@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/…",
    }
}

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:18):

I kept the exposed values outside of the record because I really like how compact the module header looks

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:30):

yeah, that looks good to me! :thumbs_up:

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:32):

What if the app one looked like this?

app [main] {
    pf: "https://…"
}

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:33):

It could work for package too:

package [ParserCore, ParserCsv, ParserStr] {
    json: "https://…/json/…",
    unicode: "https://…/unicode/…",
}

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:34):

I'm up for trying it! :thumbs_up:

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:34):

It doesn't work for platform package but that shouldn't come up that often

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:39):

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://…",
    }
}

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:41):

The multi-line list for app, package, and platform package might look a little weird but that should be uncommon

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:42):

might be fine too

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:42):

well, the multiline app case is uncommon, not sure about package

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:42):

yeah probably decently common with package

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:42):

but let's see if it's a problem in practice

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:43):

Would you format it like this?

package
    [
        ParserCore,
        ParserCsv,
        ParserStr
    ]
    {
        json: "https://…/json/…",
        unicode: "https://…/unicode/…",
    }

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:44):

yeah I think so

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:44):

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/…",
    }

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:44):

yeah I think just indenting all of it would look best

view this post on Zulip Richard Feldman (Jan 27 2024 at 20:44):

the first way

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:47):

Do you think we’d ever want to add more metadata to the package header? Like a description maybe

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:47):

I suppose a doc comment is better for that

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:48):

but I think I remember package having more fields from some other proposal

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:51):

Aha! Proposal: Package Versioning

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 20:52):

That proposal introduces the previous field:

previous 1.0.4 "abcdef234ad932.tar.br"

view this post on Zulip Richard Feldman (Jan 27 2024 at 22:05):

hrm yeah

view this post on Zulip Kevin Gillette (Jan 27 2024 at 22:27):

@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],
    # ...
}

view this post on Zulip Richard Feldman (Jan 27 2024 at 22:32):

@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.

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 22:48):

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.

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 22:49):

It feels weird to use a record field for them because they are a pattern (record destructuring)

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 22:50):

Whereas the other fields are more like literals, not patterns

view this post on Zulip Kevin Gillette (Jan 27 2024 at 22:54):

@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.

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 22:59):

@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 } -> …

view this post on Zulip Kevin Gillette (Jan 27 2024 at 23:00):

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).

view this post on Zulip Kevin Gillette (Jan 27 2024 at 23:04):

@Agus Zubiaga thanks for the example! So that reads as "echo and read are provided to menu" ?

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 23:04):

Yeah, I get that. I just think it’s weird syntactically to have something that looks like a record literal to contain a pattern.

view this post on Zulip Kevin Gillette (Jan 27 2024 at 23:16):

Or maybe I read that backwards, and it's supposed to be "echo and read are provided by main" ?

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 23:56):

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.

view this post on Zulip Agus Zubiaga (Jan 27 2024 at 23:58):

echo and read are available to the whole module, including unexposed defs

view this post on Zulip Luke Boswell (Jan 28 2024 at 00:21):

Agus Zubiaga said:

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 } -> …

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]

view this post on Zulip Luke Boswell (Jan 28 2024 at 00:30):

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.

view this post on Zulip Luke Boswell (Jan 28 2024 at 00:33):

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]

view this post on Zulip Luke Boswell (Jan 28 2024 at 00:37):

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.

view this post on Zulip Kevin Gillette (Jan 28 2024 at 01:44):

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 }

view this post on Zulip Kevin Gillette (Jan 28 2024 at 01:59):

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:

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,
    }
]

view this post on Zulip Agus Zubiaga (Jan 28 2024 at 10:59):

I'm torn. I see how it helps in that case, but I still love the simplicity of the original syntax.

view this post on Zulip Agus Zubiaga (Jan 28 2024 at 11:04):

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.

view this post on Zulip Agus Zubiaga (Jan 28 2024 at 11:09):

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.

view this post on Zulip Agus Zubiaga (Jan 28 2024 at 11:16):

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?

view this post on Zulip Richard Feldman (Jan 28 2024 at 12:06):

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