Looking Over the Horizon: A Language That Just Works
I didn't mean to design a programming language. Well, that's not true. I did. I just wasn't planning on it becoming what it became.
Horizon started as a thought experiment. A way to envision what a language would look like if you combined Rust's precision and power with C#'s expressiveness. If it ever got built, it was going to be a "because I can" project, not a "because the world needs this" kind of thing.
But that changed when the type system clicked — especially its mutability model. That's when Horizon stopped feeling like a "toy". Then came its async model. That's when I knew I had something I couldn't keep quiet about.
Yeah, I know. Another programming language. We've all seen this movie before: someone gets annoyed at their favorite language and decides to rewrite the universe from scratch. I promise: this isn't that.
Horizon wasn't born out of ego. It came from frustration, curiosity, and one dangerous question: what if things just made sense? A language that "just works" and gets out of your way.
So, a "mutability model"? What do I mean by that? Mutability is scattered; Sometimes it's a field-level thing, sometimes a language trick, sometimes it's just a convention. Horizon does something simpler: mutability is part of the value's type. Not "this field is readonly
" or "this local was declared mut
". I mean Span<T>
vs. mut Span<T>
. A single type expresses both what it is and what you're allowed to do with it. And yes, mut T
can be implicitly converted to a read-only T
.
Let me show you what that unlocks. Say you have an array of mutable objects: mut Array<mut T>
. In Horizon, that's exactly what it says. Now — say you want to hand someone a read-only view of the same array with immutable elements. In C#, you'd need to copy and wrap each element into a different array. In Horizon? You just cast it: mut Array<mut T>
becomes Array<T>
. Same heap object. No cloning and no wrappers. Just a type-level expression of intent.
But this isn't just about containers — it changes how code behaves. In the implementation of MyContainer<T>
, you can't call mut
methods on T
unless the caller has an MyContainer<mut T>
. Immutability becomes composable and structurally enforced.
It just works.
And honestly? That same principle of "it just works" made me rethink async/await.
To start: Horizon doesn't have await
. It just doesn't need it. There's no compiler-generated state machines, no continuations, not even function coloring. Everything just runs on green threads — fibers — and those fibers can suspend and resume naturally.
Go fans often say goroutines solve the async problem. And to be fair, they kind of do. But Go solves it by dodging the issue: every async operation is spun off as a new goroutine, and you coordinate with channels. That's not bad — it just pushes structure onto the caller, not the callee.
In C#, you'd write something like this:
Channel<int> c = Channel.CreateUnbounded<int>();
Task.Run(async () => await c.Writer.WriteAsync(42));
int result = await c.Reader.ReadAsync();
That works, but you've just kicked off a separate task, forced async context switches, and wrapped everything in state machines.
In Horizon? Same pattern. But no keywords, no async function declarations, and no overhead to think about:
let c = Channel.CreateUnbounded<int>();
Fiber.Run(|| { c.Writer.Write(42); });
let result = c.Reader.Read();
That's it. No Task<T>
, no async
, and no await
. Read
gives you the value directly, even if it has to suspend the fiber and wait.
Write(42)
might suspend, and so might Read()
. But the difference is: you just don't care. You write synchronous looking code, and everything just works.
And here's the best part: there's no function coloring. You don't need two versions of every API. You don't need to "await" your way up the call stack. Every function can suspend. You don't need to think about it.
It just works.
You might be thinking: "Cool story, bro. You 'invented' goroutines and green threads. You want a medal or something?" Yes, it's green threads. But not because it's clever, but because it's better.
People love to say, "at least in Go, I can see where a suspension might happen". Sure, reading from a channel might block. Same in C#; If you see an await
, you know a suspension might happen.
But here's the problem: That kind of reasoning is a trap. Nothing stops a deeper stack frame from calling Thread.Sleep(..)
, blocking on a socket, or doing some long-running computation. You're writing synchronous code, sure, but you're still vulnerable to surprise latency and long execution costs.
Horizon takes a different approach: Just stop caring. Instead of trying to spot each possible suspension, Horizon embraces it as a natural part of execution. Everything runs on a fiber. If something needs to wait, it suspends the fiber. That's it.
You stop asking "will this block" and start asking "do I care if it does?" Because even in the async/await world, everyone almost always immediately await the call. Why bother with all that ceremony? Why give up stack-local safety? Especially if we could just... not?
That last part isn't just rhetorical. In C#, you literally can't even hold a Span<T>
(or any "byref" or "byref-like" value) across an await
. And the compiler's right to stop you. Because once you await
, your function is a state machine that gets boxed onto the heap. Any stack safety is gone. That span? It's possibly pointing at garbage.
Same deal with locks. In C#, monitor-based locks support reentrancy through the thread ID. If you held a lock across an await
, you might resume on a different thread with a different ID. Bad.
But Horizon? Horizon doesn't erase your stack. Suspending a fiber doesn't mean transforming the function. It means parking it — stacks, locals, and all — until it's ready to resume.
So yes: in Horizon, you can hold a span across a Task<T>.Result
(or any other suspension for that matter). Same with a lock, too. No tricks. No flags. You wrote synchronous looking code and Horizon respected that.
It just works.
I didn't set out to reinvent anything. But somewhere along the way, between the mutability model, the asynchronous style, and the realization that stack-local safety didn't have to be sacrificed for ergonomic, Horizon stopped being a "project".
It became something I would genuinely want to use. And maybe you do too. This post isn't the full language. Not even close. There's still more to say on generics, traits/interfaces, safety, interop, and how it all fits together. For now, Horizon is a language that gets out of your way and just does what you want.
I'm not saying it's magic. I'm saying it just works. But sometimes, that feels like magic.
Horizon isn't anywhere near finished. But the ideas are there. If you've ever been frustrated by the tension between elegance and practicality in language design, maybe you'll find something here too.