Stream: ideas

Topic: calling effectful functions without `!`


view this post on Zulip Sam Mohr (Aug 28 2024 at 21:11):

It seems like if we kept the ! suffix requirement, it's an obvious await point in the code that still doesn't require putting Task in all of our signatures. The only question is if we want to hide Task, then how do we prevent the need to double suffix a la File.read!?

view this post on Zulip Richard Feldman (Aug 28 2024 at 21:12):

(splitting this off from #ideas > function effectfulness syntax )

Richard Feldman said:

Brendan Hansknecht said:

Sam Mohr said:

It does hide it, but doesn't not requiring ! hide it as well, in a way? I think this whole subject is a way to hide Tasks, basically

That is actually my biggest concern of this entire proposal. In most (all?) languages with async and await, await is required. This is for understandability. There is no reason it actually is needed in the code. Calling an async function is enough to know it must be awaited. Await could be hidden away.

Go is a notable exception; all I/O in Go is async behind the scenes and there's no await keyword (or async keyword for that matter) and I've generally seen that described as a selling point of Go

Brendan Hansknecht said:

That is not a correct view of go
And it is the reverse problem
Async to sync is always safe. Sync to async is not

@Brendan Hansknecht can you elaborate on this?

view this post on Zulip Richard Feldman (Aug 28 2024 at 21:12):

my understanding is that Go always does nonblocking I/O and manages it for you behind the scenes, and I know nonblocking is not exactly the same thing as async, but the tradeoffs seem to be the same in this particular situation (e.g. in both cases you're doing nonblocking I/O without marking in the expression that anything unusual has happened) unless I'm missing something!

view this post on Zulip Richard Feldman (Aug 28 2024 at 21:13):

I made #ideas>calling effectful functions without `!` to discuss that topic separately

view this post on Zulip Sam Mohr (Aug 28 2024 at 21:13):

Okay, I'll wait for Brendan to chime in and discuss over there

view this post on Zulip Sam Mohr (Aug 28 2024 at 21:13):

Thank goodness for topics haha

view this post on Zulip Richard Feldman (Aug 28 2024 at 21:15):

Zulip is awesome haha

view this post on Zulip Sam Mohr (Aug 28 2024 at 21:44):

An argument against requiring this is that I don't think requiring ! would work with effect-polymorphic functions. If you have the following code:

List.forEach : List a, (a -fx-> {}) -fx-> {}

main =
    Stdout.line "Counting down from 5:"
    {} = List.range { start: At 1, end: At 5 }
        |> List.forEach \count ->
            Stdout.line "$(Num.toStr count)..."
    Stdout.line "Finished!"

How would you know if you needed to put the ! on List.forEach without checking if the inner code is effectful? What if the inner function has a complex body? You couldn't just check List.forEach's type, you'd probably just wait for the compiler to tell you that you're missing a !.

view this post on Zulip Sam Mohr (Aug 28 2024 at 21:49):

Though the lack of ! still fails to make effectful code obvious, this makes it seem to me that the need to annotate ! on all effectful calls isn't so simple without tooling help

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:42):

So I think there is a very important distinction here:

turning async code into sync is always fine. It actually happens all the time. Every synchronous code base that does any sort of io is doing this. Even if it is not done at the language level it is definitely done at the OS level. When you make a network call in python with request. This is what is happening as well. The underlying network call is 100% going to be async. But the thread is being parked and blocking. This is not a special conversion. this is just the standard.


So when looking at Go. It isn't doing anything special. It is making an async network call cause all network calls are fundamentally async somewhere under the hood. It is basically doing a block_on(some_future.await). You could do this in synchronous code in rust as well. The synchronous code could use an async network library and just manually block the thread on the async future.

Go has a 100% synchronous model from the user perspective. It just has a super easy way to span a new thread. As such, you can always span a new thread and have it block on a synchronous task leaving your main thread unblocked.


Roc is not doing this, it is doing the reverse. Instead of converting async code to sync by blocking, it is converting sync code to async implicitly. You also don't have an escape hatch to easily spawn threads in Roc.

view this post on Zulip Brendan Hansknecht (Aug 28 2024 at 22:43):

How would you know if you needed to put the ! on List.forEach without checking if the inner code is effectful? What if the inner function has a complex body? You couldn't just check List.forEach's type, you'd probably just wait for the compiler to tell you that you're missing a !.

Yeah, I was thinking you would just get a compiler error and add the !.

view this post on Zulip timotree (Aug 29 2024 at 01:25):

If we end up with a Roc where effectful calls are not visually marked with ! in the source code, maybe there could be type-aware highlighting that changes the color of the function name at the callsite if its type tells you it's effectful.

view this post on Zulip Sam Mohr (Aug 29 2024 at 01:34):

tree-sitter should have a way to do this. It already highlights defined variables differently than undefined ones!

view this post on Zulip Sam Mohr (Aug 29 2024 at 01:35):

But that's within the context of a single function, this is more complex

view this post on Zulip Sam Mohr (Aug 29 2024 at 01:35):

We could require some naming change to make it obvious, like, I don't know, an exclamation point after the function name?

view this post on Zulip Brendan Hansknecht (Aug 29 2024 at 02:03):

Haha

view this post on Zulip Brendan Hansknecht (Aug 29 2024 at 02:03):

:joy:

view this post on Zulip Agus Zubiaga (Aug 29 2024 at 03:15):

Isn’t tree-sitter just a parser? I think this requires at least canonicalization and looking into other files

view this post on Zulip Agus Zubiaga (Aug 29 2024 at 03:16):

It might be doable through the LSP, though

view this post on Zulip Agus Zubiaga (Aug 29 2024 at 03:16):

inlay hints probably

view this post on Zulip Brendan Hansknecht (Aug 29 2024 at 03:47):

It could at least distinguish higher order from normal functions. It could also distinguish more if it had a type signiture. At least assuming it can look at a larger context


Last updated: Jun 16 2026 at 16:19 UTC