Stream: ideas

Topic: hello world snippet friendliness


view this post on Zulip Richard Feldman (Jul 31 2025 at 19:42):

I had an idea for a revision to the app module header syntax (and basic-cli entrypoint API) that conveys the same information as today, but in a way that makes Hello World more concise and approachable:

Screenshot 2025-07-31 at 3.37.56 PM.png

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:42):

so it's just 2 lines of code now

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:43):

but the horizontal scroll bar is intentionally hiding stuff. after explaining what this part that's visible means, the tutorial could then tell you to add a comma before the } at the end of the first line, which would make roc format change it to:

Screenshot 2025-07-31 at 3.39.47 PM.png

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:49):

so the main changes to the app header are:

these have the goal of making the relevant parts of the platform URL (domain, package name, version number) visible instead of running off the end of the screen. The hash and file extension still run off the end, but those are generally not interesting anyway.

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:49):

finally, there's also this:

main! = |{ stdout }| stdout.line!("Hello, World!")

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:50):

the idea here is that, similar to #ideas > fs API using static dispatch, what if the platform just provides all these I/O values like fs, stdout, etc. in main?

edit: we'd also pass in args and env to that record, so in a super featureful scenario you could do like:

main! = |{ args, env, stdout, stderr, stdin, fs, http }|

or if you didn't want to destructure, just:

main! = |cli|

...and then do like cli.args, cli.env, etc.

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:53):

this makes all the I/O testable, and also makes it obvious which functions are doing I/O - basically all the benefits of #ideas > fs API using static dispatch

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:56):

at first I thought that would be annoying in scripts, because you'd have to pass fs, stdout, etc. into your top-level functions...but then I realized - they're scripts, and it's all gonna be in one file anyway, so just put those functions inside main! and close over them, easy peasy

and this is actually more concise overall for scripts, because instead of having to add an extra line of import cli.Stderr to start using stderr, you can just add stderr to the existing destructure to bring it into scope

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:57):

one thing I didn't mention here is that I think there's a cool opportunity to use a feature we've always had but haven't made much use of, namely the fact that you can put a # at the end of the URL to specify which .roc file you want to use as the platform module

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:57):

so for example:

...tar.bz#safe.roc

this could be how you get "safe script" mode - it's not a separate platform, it's literally basic-cli - just a different entrypoint

view this post on Zulip Richard Feldman (Jul 31 2025 at 19:58):

so you just add #safe.roc onto the end of that URL and now you're in safe-script mode for whatever basic-cli script you downloaded off the internet

view this post on Zulip Richard Feldman (Jul 31 2025 at 20:10):

and this way you don't need two sets of docs, or wondering whether they are actually API-compatible, etc...

view this post on Zulip Richard Feldman (Jul 31 2025 at 20:21):

anyway, curious what anyone's thoughts are on this!

view this post on Zulip Hubbard Joppa (Jul 31 2025 at 21:21):

Richard Feldman said:

the idea here is that, similar to #ideas > fs API using static dispatch, what if the platform just provides all these I/O values like fs, stdout, etc. in main?

Does this mean if there were other files, the other files would no longer be able to import these sorts of things and instead receive a record as a param in any of their functions that require I/O / any platform functionality?

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:20):

right

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:21):

So do you still import things with pf.Stdout?

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:21):

no need in this design

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:21):

instead you pass it around

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:21):

same design Zig is moving toward

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:21):

I'm probably going to need a more complete app and platform module to really grok this sorry

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:22):

sure, got a basic-cli app I could port?

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:22):

Also re the changes to the header... I think it would be nice to use "normal" syntax as much as possible. So like if we could make it a record that would be easiest for people to understand.

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:23):

The simplest test platform I typically use is my "template" one https://github.com/lukewilliamboswell/roc-platform-template-zig/tree/main/platform

https://github.com/lukewilliamboswell/roc-platform-template-zig/blob/main/examples/hello.roc

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:25):

We only need two files in this new setup I think, app.roc and platform.roc

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:25):

oh that one's trivial haha

app [main!] { pf: platform "../platform/main.roc" }

import pf.Stdout

main! : {} => Result {} _
main! = |{}|

    Stdout.line!("Roc loves Zig")

    Ok({})

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:26):

becomes:

app { cli: "../platform/main.roc" platform [main!] }

main! : cli.Fx => Result {} _
main! = |{ stdout }|
    stdout.line!("Roc loves Zig")

    Ok({})

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:27):

(I don't know if Fx is the right type for that thing that gets passed in, but that can be figured out separately)

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:28):

Do you have thoughts on the platform header too?

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:31):

not really

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:31):

at least not in this thread haha

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:31):

mainly it's just been on my mind since writing the original tutorial that we kind of hit beginners with a lot up front compared to most other languages

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:32):

and I've been trying to think how to make it more approachable

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:33):

Would this be valid?

app {
    pf: "../platform/main.roc" platform [main!],
    json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.13.0/RqendgZw5e1RsQa3kFhgtnMP8efWoqGRsAvubx4-zus.tar.br",
}

main! : pf.Io => Result {} _
main! = |{ stdout }| {

    jsonStr = Str.toUtf8 "{\"name\":\"Röc Lang\"}"

    decoded : Result({ name : Str }, _)
    decoded = jsonStr.fromBytes(Json.utf8)

    stdout.line!("${decoded?.name} loves Zig")

    Ok({})
}

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:34):

The advantage of app [main!] was that is similar to modules where it exports or provides the main!

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:36):

the similarity is kind of an upside and a downside though

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:36):

the downside is that it looks like it's exposing it for other modules to import, but it's not

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:36):

app { makes it more obvious that you can't import anything from this

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:37):

Maybe this is silly... but I feel like a record could be more expressive

app { pf: "../platform/main.roc" platform [main!] }

app { pf: { platform: "../platform/main.roc", requires: [main!]} }

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:38):

I'd love the app & modules & platform & package headers to feel like LEGO blocks that plug together and they all look like they have shapes that "fit"

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:43):

one of my original goals here is to see if we can have:

there are lots of possible designs out there that have various attributes, but to be honest I've never really felt like "wow this app header design is just so much better than the others!"

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:43):

so if they're all pretty close, why not pick a design from among them that has benefits to beginners?

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:44):

  1. display the longer platform string on a single line - I don't mind, I'd kind of prefer to be able to see it all
  2. display platform on another line - I don't love this, I prefer it all on a single line or "together"
  3. dropping the https:// - sounds good, I wonder if there are other things we may want to support in future and assuming https might lock us in
  4. roc-lang.github - nice simplification
  5. passing IO as an argument (instead of importing the module) - I like this idea -- needs exploration to know how it feels working with multiple modules/packages
  6. the # feature for different "sub-platforms" - interesting... I'd like to understand this more

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:44):

the # thing has always been there, we just haven't really used it haha

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:45):

it's really simple: the url links to a tarball of various files, and one of them is a .roc file that's a platform module and that's your platform module

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:45):

by default we choose main.roc but you can do #whatever.roc at the end of the url to pick a different .roc file in the tarball to use as your platform module

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:46):

Ah, scheme is the word I was looking for - ftp://, file:// ??

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:46):

that's it, and that's always been supported (as part of my "no magical entrypoint names; you can always choose whatever name you want for your .roc files)

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:46):

the realization I had was that this could be used to do variations - like #safe.roc to opt into sandboxed CLI where it prompts the end user before doing I/O

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:47):

and also we could do like #batch.roc to do something that never deallocates memory and just uses a giant bump allocator for the duration of the entire process, under the assumption that it's not going to be long-running

view this post on Zulip Richard Feldman (Jul 31 2025 at 22:47):

etc.

view this post on Zulip Luke Boswell (Jul 31 2025 at 22:48):

So it defaults to say platform.roc (kind of like index.html for a web server) but if you add a hash it uses a different platform module within the platform.

I asume all these platforms would use the same entrypoints and share a host? or maybe we do the thing where the platform module tells roc where the binaries are located... so a platform could ship with multiple host.a included??

view this post on Zulip Richard Feldman (Jul 31 2025 at 23:31):

Luke Boswell said:

So it defaults to say platform.roc (kind of like index.html for a web server) but if you add a hash it uses a different platform module within the platform.

well the default has always been main.roc and I don't think we should change that, because it's how platform authors can get the experience of just running roc check with no arguments since that also defaults to main.roc :smile:

view this post on Zulip Richard Feldman (Jul 31 2025 at 23:33):

Luke Boswell said:

I asume all these platforms would use the same entrypoints and share a host? or maybe we do the thing where the platform module tells roc where the binaries are located... so a platform could ship with multiple host.a included??

it's up to the platform module - the tarball can contain whatever files the platform author decides to put in there, and then the platform module determines which of those files it wants to use

view this post on Zulip Richard Feldman (Jul 31 2025 at 23:33):

one way to think of it: a tarball just unpacks into a folder, and then whatever files in there work as normal

view this post on Zulip Richard Feldman (Jul 31 2025 at 23:34):

and roc asks the application what .roc file to use for the application's platform module and then just looks at that file for what to do next

view this post on Zulip Richard Feldman (Jul 31 2025 at 23:35):

so if there are other files hanging around in that same directory, it doesn't really know or care

view this post on Zulip Richard Feldman (Jul 31 2025 at 23:36):

similarly if some of those other files happen to be .roc files that are platform module, and some of those happen to reuse the same host files, again roc doesn't know or care

view this post on Zulip Luke Boswell (Jul 31 2025 at 23:37):

Yes, but currently the platform host is hardcoded and assumed to be sitting next to the main.roc. I think our plan is to have that anywhere inside the tarball folder... and the platform header has some way to say, go get my linux-x64 archive from here

view this post on Zulip Luke Boswell (Jul 31 2025 at 23:37):

I think we're on the same page

view this post on Zulip Luke Boswell (Jul 31 2025 at 23:38):

In summary I think app { pf: "../platform/main.roc" platform [main!] } is worth trying :thumbs_up:

view this post on Zulip Kilian Vounckx (Aug 01 2025 at 04:28):

I'm not sure about passing platform functions to main. It makes 'normal' imports and platform imports feel different. But that might be a good thing as well. I'd need to see and use the proposed way to get a proper opinion.
The new header looks a bit cleaner to me though, so I like that

view this post on Zulip Jared Ramirez (Aug 01 2025 at 05:29):

I like the explicitness of having main accept platform-specific args! And if those args are the primary ways a user create effectful functions, I think this would encourage more pure functions in user code (since passing those args everywhere adds friction)

The downside I see is mainly verboseness. I could see it being a little painful in larger codebases

view this post on Zulip Austin Davis (Aug 01 2025 at 06:56):

I think now is a good time to mention that OCaml's module system basically solves the verbosity problem of passing around effectful functions and configuration data between modules. Basically they have two kinds of modules: static modules and "functor" (terrible name IMO) modules. The functor modules are basically just functions that can accept one or more modules and return a new module.

Here is an example. I'll use ReasonML (an alternate syntax for OCaml) just because it probably feels more familiar to most people:

module WithNetwork = (NetworkIO: {
    let send: string => unit;
    let listen: (string => unit) => unit;
}) => {
    let sendStuff = NetworkIO.send;
    let listenToStuff = NetworkIO.listen;
    module Internal = NetworkIO;
};

Here is the ReasonML playground link to check it out:
https://reasonml.github.io/en/try.html?rrjsx=true&reason=LYewJgrgNgpgBAdQJYBcAWA5GKDuIBOA1nALwBQclVcAFBddVrgYQJIDyAXHAN70PVYKOAGcYAOzDcRKfEnEBzUgD44EcagDc-AZSFwoSGRO40ZcxSrUaUASivqtOhgF8ANM8r2SqvnuyiEmAAyigQAGbhpHBMeEQcAHRiktr+wobG4gAqIKERUSQx2HFs7AkZKBKpcKCQsHCs4pX44gCGUNGxLBzaLtpAA

view this post on Zulip Austin Davis (Aug 01 2025 at 07:01):

Not sure if this is the direction you guys want to go with this, but I've always found this to be incredibly useful, as it's basically like OOP-style dependency injection but without all the weird OOP baggage.

This allows you to have module-scoped types/values so you can do the dependency injection at a macro level and avoid having to pass in those dependencies to each individual function.

view this post on Zulip Austin Davis (Aug 01 2025 at 07:23):

We can already kinda do this in Roc with just plain functions that take/return structs of functions, which we can treat as "modules":

create_api :
    {
        send : Str -> {},
        listen : (Str -> {}) -> {},
    }
    -> {
        addPrefixAndSend : Str, Str -> {},
        init : {} -> {},
    }
create_api = |{ send, listen }|
    addPrefixAndSend = |prefix, msg|
        send(Str.concat(prefix, msg))
    init = |_|
        listen(|value| addPrefixAndSend("🐈 ", value))
    { addPrefixAndSend, init }

view this post on Zulip Austin Davis (Aug 01 2025 at 07:33):

The point is, we can use the above style to kinda get around some of the function-passing for platform APIs.

And I would strongly encourage new users to do it this way, especially as they build their own domain-specific IO utilities. That way they can still get all the testability benefits without exposing half of their codebase to that function-passing boilerplate.

This is the de facto standard way of designing APIs in OCaml/ReasonML, and for good reason.

view this post on Zulip Austin Davis (Aug 01 2025 at 07:40):

Technically OCaml's module system is more powerful at the type level (e.g. you can simulate higher-kinded-types with functor modules), but that isn't really necessary here.

It would be pretty cool if we could actually do dependency injection at the module level (without OCaml's fancy type-level stuff), just to make this pattern feel more "official" and avoid having to use both modules and records as API namespaces. But it definitely isn't necessary.

view this post on Zulip Anton (Aug 01 2025 at 09:45):

This is actually currently implemented in the old compiler, we call it module params: https://github.com/roc-lang/roc/blob/main/crates/cli/tests/test-projects/module_params/Api.roc

view this post on Zulip Anton (Aug 01 2025 at 10:00):

I'm on board with what's suggested in the proposal, although I'd also like to have a proper record instead of: { pf: "../platform/main.roc" platform [main!] }. In the tutorial and when teaching things to beginners we can format things how we want, so have the record on a single line. I feel like it's ok for the formatter to always turn that into multiple lines.

view this post on Zulip Luke Boswell (Aug 01 2025 at 10:01):

@Anton did you have any ideas for a proper record?

Did you like my suggestion here? #ideas > hello world snippet friendliness @ 💬

view this post on Zulip Anton (Aug 01 2025 at 10:03):

Yeah, perhaps it can be even further simplified:

app { pf: "../platform/main.roc", requires: [ main! ] }

view this post on Zulip Luke Boswell (Aug 01 2025 at 10:05):

How do we know what are the Packages other than the platform package?

view this post on Zulip Anton (Aug 01 2025 at 10:28):

Based on the requires:? We'd need to make it a list as well:

app [{ pf: "../platform/main.roc", requires: [ main! ] } ]

view this post on Zulip Hubbard Joppa (Aug 01 2025 at 12:54):

I'm going to take a wager that 1) the changes to the platform/app header make it seem simpler and don't really seem to have any obvious downsides 2) beginners will find it more confusing to import some things (like in every other language) but take other things as parameters to main!. It introduces a split every time they think "I want to use this thing, oh I'll import it - wait, should I do the param-style "import" or the normal way?"

I do also share some of the concerns around introducing extra verbosity for effectful code. I've dealt with some Haskell that was written in a record-of-functions style and it gets a little tedious to have an extra parameter for every effectful function (probably a main driver for why mtl and effect systems at the type level are way more popular).

view this post on Zulip Hubbard Joppa (Aug 01 2025 at 12:57):

Austin Davis said:

The point is, we can use the above style to kinda get around some of the function-passing for platform APIs.

I'm curious to learn more about this - right now I'm a little confused how the example gets around function passing. Once you call create_api and get back a record, don't you still have to pass that record as a parameter to all functions that want to use the API?

Edit: I think I see now - you would essentially write the entire module inside the outer function, treating the outer function like a functor in the OCaml/Reason example. Makes sense.

view this post on Zulip Richard Feldman (Aug 01 2025 at 13:09):

Hubbard Joppa said:

I do also share some of the concerns around introducing extra verbosity for effectful code. I've dealt with some Haskell that was written in a record-of-functions style and it gets a little tedious to have an extra parameter for every effectful function (probably a main driver for why mtl and effect systems at the type level are way more popular).

yeah that's a downside, but it's by far the simplest way I've found to make I/O logic actually testable without having to run the real I/O, and I really want to encourage that style of testing. I like that this makes it really obvious how to do that.

view this post on Zulip Richard Feldman (Aug 01 2025 at 13:10):

I've had a good experience using that style at Zed in Rust, we also used it at NoRedInk in Haskell to good effect (I think @Jasper Woudenberg introduced it if I remember right?) and Zig is also about to start doing it in the stdlib.

view this post on Zulip Richard Feldman (Aug 01 2025 at 13:13):

I've never seen a nicer way to test effects in any programming language, and I think it's worth trying out as a default rather than something that people end up having to bolt onto a foundation that has no story for simulating effects

view this post on Zulip Hubbard Joppa (Aug 01 2025 at 13:43):

True, if there's no approach presented early on for the community to coalesce around, things will get messier.

view this post on Zulip Hubbard Joppa (Aug 01 2025 at 13:50):

Richard Feldman said:

I've never seen a nicer way to test effects in any programming language, and I think it's worth trying out as a default rather than something that people end up having to bolt onto a foundation that has no story for simulating effects

Algebraic effect systems are pretty beautiful in this regard IMHO. The fancy name makes it sound overly complex, but it's just a list of types of effects used by a function, and then callers can specify the implementation. It kind of goes along with Roc's ->/=> distinction, but would specify the name of effects used by the =>. Might be more confusing for beginners, though, I'm not sure.

view this post on Zulip Hubbard Joppa (Aug 01 2025 at 13:53):

Austin Davis said:

It would be pretty cool if we could actually do dependency injection at the module level (without OCaml's fancy type-level stuff), just to make this pattern feel more "official" and avoid having to use both modules and records as API namespaces. But it definitely isn't necessary.

Did you have something in mind for the syntax that would make this pattern more "official"? It seems like your code snippet pretty much covers all the bases. I think if Roc goes in the direction Richard's leaning toward w.r.t not importing effects, I'd probably end up structuring all my code like this.

view this post on Zulip Richard Feldman (Aug 01 2025 at 14:45):

Hubbard Joppa said:

Algebraic effect systems are pretty beautiful in this regard IMHO. The fancy name makes it sound overly complex, but it's just a list of types of effects used by a function, and then callers can specify the implementation. It kind of goes along with Roc's ->/=> distinction, but would specify the name of effects used by the =>. Might be more confusing for beginners, though, I'm not sure.

yeah we've talked about that. We intentionally decided to go wit this "binary algebraic effects" (-> vs =>) instead of a full OCaml/Koka/Unison-style algebraic effects system with handlers and effect polymorphism.

I think the main tradeoffs here are:

view this post on Zulip Richard Feldman (Aug 01 2025 at 14:48):

also of note, in many cases you're passing around some state anyway and don't need an extra argument - e.g. you can have my_state.fs.read!(...) and not need to pass fs as a separate argument since you're already passing my_state around

view this post on Zulip Richard Feldman (Aug 01 2025 at 14:51):

granted, I haven't used algebraic effects in a sizable code base in anger, but it feels like the selling point for this scenario (still change the type but don't change the call site to need an extra arg) is not worth the language complexity

view this post on Zulip Anton (Aug 01 2025 at 14:55):

Is the goal to make this work with module params too? I vaguely remember we scrapped that for the new compiler.

view this post on Zulip Richard Feldman (Aug 01 2025 at 16:09):

the goal is to do this instead of module params

view this post on Zulip Richard Feldman (Aug 01 2025 at 16:09):

module params, algebraic effects, and static dispatch can all be used to get (imo) similar levels of ergonomics

view this post on Zulip Richard Feldman (Aug 01 2025 at 16:11):

as usual, I'm happiest if we can just do it with static dispatch instead of adding other language features to get approximately the same ergonomics :smile:

view this post on Zulip Richard Feldman (Aug 01 2025 at 19:57):

to give an idea of a small but nontrivial example with this style, I asked Claude to port our bash script for building the website to Python, and then ported that to Roc in this style:

view this post on Zulip Richard Feldman (Aug 01 2025 at 20:30):

(I made up some Roc APIs that don't exist yet, e.g. streaming I/O)

view this post on Zulip Luke Boswell (Aug 01 2025 at 20:39):

I like the look of that Roc script :grinning_face_with_smiling_eyes:

view this post on Zulip Austin Davis (Aug 01 2025 at 22:19):

I agree with our dear BDFN, I think this is a really nice design choice. I would definitely appreciate having module params just to make things feel more consistent when doing that dependency injection pattern, but I do think it adds some complexity (to both the compiler and the language), and it probably feels a bit foreign to a lot of newcomers who haven't used OCaml/ReasonML.

view this post on Zulip Austin Davis (Aug 01 2025 at 22:33):

I also just want to say, I really REALLY appreciate all the work and mental energy that was put into this language, and I can't emphasize enough how nice it is to have a simple & expressive funcitonal language that runs wicked fast.

My elevator pitch for Roc right now is that it's basically all the best things about Elm, Rust, and Lua, but without all the bad stuff. I can mostly just write data types and functions and just get shit done. I don't have to think too hard because it's such a simple language. I have even moved away from Bash/Python for scripting in favor of Roc. It just feels great to work with it. Still needs some sanding around the edges, but dang, I'm so impressed!

view this post on Zulip Richard Feldman (Aug 01 2025 at 22:44):

thanks! It's been a lot of work, but I'm really excited about where we've gotten to, and I'm stoked to see what people think of it for Advent of Code assuming we achieve our goal of having the new compiler usable for it :grinning_face_with_smiling_eyes:

view this post on Zulip Niclas Ahden (Aug 01 2025 at 23:26):

fwiw I use the style of passing in effectful functions as an argument in my Roc apps and it’s been nice. I’ve ended up putting my most commonly used effects in a single record that I pass to most effectful functions. Then each function sig uses the record matching syntax to spell out which effects it needs. Turn out really neat, ergonomic, and clear :+1:

view this post on Zulip Luke Boswell (Aug 02 2025 at 00:25):

@Niclas Ahden do you have any examples or snippets you could share?

view this post on Zulip Niclas Ahden (Aug 02 2025 at 08:59):

Probably! I’m on vacation right now but I’ll try to remember to return to this when I’m back :)

view this post on Zulip Anthony Bullard (Aug 06 2025 at 11:13):

One thing that I love about this is that if Std{In|Out|Err} becomes just a interface then you would be able to create applications that are capabilities secure to your Roc dependencies. So even without safe script you could limit what deps could do with IO.

view this post on Zulip Anthony Bullard (Aug 06 2025 at 11:17):

This was one of the core features of Chakra, except it did it by having the IO functions in the stdlib, but they all required capabilities and those capabilities could not be constructed in Chakra code, they were given to the main actor in it's init function. There were then functions that could create more restricted versions of those capabilities to pass on to other functions. They would fail if you tried to broaden the capability(say authorize the filesystem capability to include a sibling or parent directory)

view this post on Zulip Anthony Bullard (Aug 06 2025 at 11:19):

So, io.stdout.write had the signature io.stdout.write(StdoutCap, String) Cmd (Chakra had managed effects like Elm). If you weren't passed that capability, you couldn't write!


Last updated: Jun 16 2026 at 16:19 UTC