Those who know me know that I spend a lot of time thinking about programming language design, and what the next generation of programming languages that push the industry forward might look like. Today I’ll be talking about my two favorite language features I keep coming back to, which I think will define what the next 10 years look like in programming language design.
Functions Should Return Monads or Effects
To anyone unfamiliar with what a Monad is, I’d recommend reading one of the many monad explainers that exist on the internet, I don’t have the hubris to think I’m the person to make the perfect explainer for beginners. At least not today.
This is a take that’s probably unsurprising to anyone following the space, but Monads work well for representing things like optional values, errors, and IO, and I think languages would do well to go all in on them. Most languages solve these problems individually, Go has greenthreads for IO, Python and Javascript have async-await and thrown Errors (that don’t get recorded in the type signature), Java has its Billion Dollar Mistake for nulls. Having a single solution for all of these, and being able to embed it directly into the type system with a return type like IO<Result<Maybe<int>, HttpError>> does feel like a step forward for the industry.
The obvious question people will bring up with this is sure, but why does that need to be Monads? Rust doesn’t support Monads, and it handles Result and Maybe just fine, and Monads don’t compose so you’re just giving yourself a bunch of dealing with Monad transformers, type-system astronauts, and overhead for nothing! Yes, I don’t disagree with any of that, but after several years of working with Rust’s ? operator, I think those problems aren’t as big as people think, and for two reasons:
First, I don’t think Monads not composing is a real problem. Generally if you have IO, you’ll want to await it, if you have a result, you’ll want to use it or raise the error up the stack, if you have an optional, you’ll want to use it or return null. Having explicit control over these is the point of using monads, combining effects is rarely a problem in practice, and maybe I’m just inexperienced in the space, but I’ve yet to personally encounter a place where I’d want a monad transformer.
Two, I think we can make the ergonomics better. Imagine a Rust-like language, but let’s add do-notation similar to Haskell. Instead of Haskell’s <- operator, we’ll use a postfix ? similar to Rust’s result operator.
1
2
3
4
5
6
fn read_file(path: string): IO<string> {
do {
const file = open(path)?;
return file.read()?;
}
}
Pretty generic, not treading new ground here but potentially need nested do blocks for different monads, and every function returning an IO will need a do block, and people already think async-await is a bit verbose. Where I think we can make it better is some syntactic sugar by adding two rules to the compiler:
- If a function returns a monad automatically insert a
doblock. - If someone uses the
?operator and it doesn’t match the currentdoblock, try topureit until it does.
As an example, let’s take a function that returns a IO<Result<Maybe<int>, HttpError>> and wrap it with another function that logs, then returns the results.
1
2
3
4
5
6
7
8
9
10
fn function_that_does_things(): IO<Result<Maybe<int>, HttpError>> {
// ...
}
fn wrapper(): IO<Result<int, HttpError>> {
log.info("calling function_that_does_things")?; // question mark handles IO, similar to await.
const result: Maybe<int> = function_that_does_things()??; // automatically handle IO and Result, if it's an Error, pure that into IO.
const return_value: int = result.or_else(4);
return return_value; // compiler converts to IO.resolved(Result.ok(return_value)), similar to how many languages handle async functions returning non-promise values.
}
This does have the downside that if you have a Maybe<Maybe<value>> you’d need to manually handle returning a Some(None) because the compiler cannot differentiate which None you’d be referring to, and it cannot handle behavior other than pure, if you needed to transform the result in any way, but as syntactic sugar this should cover most use cases. I’m sure there are people who will say having two or three question marks after every function call that does IO (since most IO can fail) is a wild choice, but I challenge them to come up with a better bang for your buck on encoding side effect information into types while still maintaining explicit control.
Monads or Effects
Anyone who follows PL design closely will think I’ve talked a lot about monads for being oddly quiet on languages with effects systems, like Effekt or Koka. These also embed side effect information into the type signature without many of the functional programming drawbacks of monads. I tend to not draw a distinction here, many effects systems are just pre-monad-transforer’ed versions of common monads (Reader, Error, IO, …) into a single concrete that you can use as “effects”. To me that sounds like a reasonable high-level language vs low-level language problem, and I think effects make a lot of sense to be the default if you’re trying to write a language to kill python vs if you need absolute control at all times, go pure monads all the way.
Functions take in Capabilities
This is a less well known take, but I think just as important if you believe it makes sense to encode “what” a function does into its type signature. Imagine a language that removes the ability to call the file open() function, or any other function that has side effects, from arbitrarily anywhere in the code. Instead, the program’s main() function takes in the program’s only reference to the outside world, and if you want your program to have any side effects, you must pass that reference around (or some subset of its functionality).
1
2
3
4
5
6
7
8
9
10
11
fn read_file(fs: FileSystem, path: string): string {
const file = os.filesystem.open(path);
return file.read();
}
fn main(argv: string[], os: OS): int {
const contents = read_file(os.filesystem, "foo.txt");
os.print(contents);
const time = os.datetime.now();
return 0;
}
In this example, the program takes in an OS object, which has attributes for every major type of IO that the program might need. These properties, like filesystem, datetime, network, etc. are capabilities, and because they are the only way to perform their respective types of IO, you can be guaranteed by the type system that anything that doesn’t take in os.network cannot perform network operations. This has two main benefits: substitution and sandboxing.
Subsitution
Because you implement os.network as an object you can only reference as an interface, not as a grounded type (dyn Trait in rust) this makes all IO in the language swappable. You can trivially pass in a mock for testing anywhere without having to do any monkey patching, solving a common issue in compiled languages where monkey patching isn’t possible. You can also pass a different object that meets the interface, giving you full control over IO. Want to log all network calls? Easy, just pass a wrapped os.network instance. Want to use an in-memory filesystem rather than a real one for a file heavy library? Done, just pass in an in-memory filesystem that implements the same methods as os.filesystem.
Sandboxing
Because you cannot make network calls without an os.network instance, you can reason about functions just by their type signature. The can be useful for your own code, but it becomes essential for future languages trying to defend against things like supply chain attacks. If you use a open source package like leftpad and it becomes compromised due to supply chain attacks, you can be sure that it won’t make network calls unless you specifically gave it the capability to do so.
This also makes every language the perfect Lua killer, because you can embed it and know it can only have the capabilities you pass it. The worst thing it could do without capabilities is to run a hot loop and take up processor time.
Type Signatures Should Tell You What the Function Does
It is wild to me, that in 2026 I can see a Go function with signature:
1
func leftpad(input string, length int) string
and not have any way of knowing if it does a blocking network call. The longer I am in this industry, the more I find that better upfront planning on types leads to better software, and that types are themselves a form of software architecture. It’s definitely possible to go too far with encoding properties in type information, anyone with experience in dependent types can tell you that determining what properties are worth reasoning about is the real problem, but I think the effects-capabilities model I’ve described here is a good trade off until we as an industry have the experience to take it to someplace even better.