This is an automated archive made by the Lemmit Bot.

The original was posted on /r/programminglanguages by /u/smthamazing on 2024-10-12 19:20:22+00:00.


I’m exploring the design space around syntax that simplifies working with continuations. Here are some examples from several languages:

The first two only work with types satisfying the Monad typeclass, and implicitly call the bind (also known as >>=, and_then or flatMap) operation. Desugaring turns the rest of the function into a continuation passed to this bind. Haskell only desugars special blocks marked with do, while Idris also has a more lightweight syntax that you can use directly within expressions.

The second two, OCaml and Gleam, allow using this syntax sugar with arbitrary functions. OCaml requires overloading the let* operator beforehand, while Gleam lets you write use result = get_something() ad hoc, where get_something is a function accepting a single-argument callback, which will eventually be called with a value.

Combining these ideas, I’m thinking of implementing a syntax that allows “flattening” pretty much any callback-accepting function by writing ! after it. Here are 3 different examples of its use:

function login(): Promise> {
    // Assuming we have JS-like Promises, we "await"
    // them by applying our sugar to "then"
    var username = get_input().then!;
    var password = get_input().then!;

    // Bangs can also be chained.
    // Here we "await" a Promise to get a Rust-like Option first and say that
    // the rest of the function will be used to map the inner value.
    var account = authenticate(username, password).then!.map!;

    return `Your account id is ${account.id}`;
}

function modifyDataInTransaction(): Promise {
    // Without "!" sugar we have to nest code:
    return runTransaction(transaction => {
        var data = transaction.readSomething();
        transaction.writeSomething();
    });

    // But with "!" we can flatten it:
    var transaction = runTransaction!;
    var data = transaction.readSomething();
    transaction.writeSomething();    
}

function compute(): Option {
    // Syntax sugar for:
    // read_line().and_then(|line| line.parse_as_int()).map(|n| 123 + n)
    return 123 + read_line().andThen!.parse_as_int().map!;
}

My main question is: this syntax seems to work fine with arbitrary functions. Is there a good reason to restrict it to only be used with monadic types, like Haskell does?

I also realize that this reads a bit weird, and it may not always be obvious when you want to call map, and_then, or something else. I’m not sure if it is really a question of readability or just habit, but it may be one of the reasons why some languages only allow this for one specific function (monadic bind).

I’d also love to hear any other thoughts or concerns about this syntax!