Me

I'm Brandon Smith, a programmer in Austin, Texas. More about me.

   

Bagel Bites 🥯 (Update on the Bagel Language)

3672

It's been about four months since I last posted about Bagel, the new JavaScript-targeted programming language I've been working on. A lot has changed since then, but things are finally crystallizing and getting into a clear-enough space where I feel comfortable sharing some concrete details (and real code!).

The past four months of this process have been a whirlwind. The compiler architecture has been turned on its head several different times as I figure out what it takes to build a static type-checker. Bagel's design, too, has gone through significant re-thinks. The core goals remain the same as they were in the original post, but some of the specific plans have gone out the window when it comes to individual features and semantics.

So first I want to talk about what has and hasn't changed, and then I want to finally present some examples of real, functioning Bagel code.

What hasn't changed #

Bagel is still a statically-typed language that compiles to JavaScript. It still has a hard separation between side-effect-free functional code, and side-effect-y procedural code. It still has reactivity to the mutation of application state/data as a first-class citizen. It still aims to be approachable and familiar to people who already know JavaScript/TypeScript, and to maintain as much of the semantics of those as possible while refining them, expanding them, and sanding off the rough edges.

What has changed #

Here are some things I talked about in the original post that have since changed:

Components/classes/stores #

This part of the design went through quite a journey, including a few crises about whether the whole project was even going to work at all, but it ended up in a place I'm pretty happy with.

Here's where I started out: classes can be useful as holders of mutable state. Mutable state should be kept small, but that small nugget is embraced by Bagel as a core part of its philosophy. Therefore, classes should have a place too. In particular: MobX embraces classes as both global stores, and for (React) UI components with observable state. It seemed like a no-brainer.

But here's the rub: Bagel isn't aiming to be a DSL for React-style UIs, I want it to be a general-purpose language suitable for games, web servers, scripting, even compilers. That includes its reactivity system. So this means that Bagel entities - and classes in particular - can't benefit from any kind of lifecycle awareness. I tried figuring out a way to track the lifecycle of class instances statically and it started to look like I would have to write a complete borrow-checker with ownership rules and everything, and to put it lightly, that just wasn't something I was interested in even attempting to do.

Okay, so no lifecycle hooks, so what?

Well the thing is: when you set up a MobX-style reaction, it can't be cleaned up automatically. It has to either have a global lifetime, or be disposed of in some way (in practice, usually on componentWillUnmount()). Otherwise, you get memory-leaks galore.

I want Bagel to be free of footguns; requiring the user to clean up their reactions manually, without a standard way of doing so, sounds like a footgun. And automatically cleaning up reactions with dynamic lifetimes just wasn't feasible.

So: reactions became global-only. That's where we are today; reactions can only be set up at the module level, and they live forever. This sounds limiting, but the thing is, reactions are really just for bridging your reactive code to the outside world. Inside your own logic, you don't really need them. In fact, I plan on forbidding reactions from changing application state at all; they can only observe application state and cause side-effects in the outside world. And the thing about the outside world, is... it's global.

Okay, so we have global-only reactions, but how is a global reaction going to observe state tucked inside a transient class-instance somewhere? It isn't, really.

So then I moved to, instead of having classes, you only have global class-like singletons called stores. These looked like classes - they had members of the usual kinds, some of those could be private, etc - but they could only exist as a global singleton. This made them better suited to global reactions.

But, eventually I realized that this was a bit silly. A store was really just another namespace inside of a module that had some different syntax. Things would be much simpler (for both language-learners and the language-implementer!) if I did away with the store concept and just allowed plain-data let declarations at the module level instead. Instead of private state/members you could just have non-exported declarations. For readability I added entire-module imports, so you can still do store.foo() if you want.

Global-only state and a total lack of components may sound bad, but here I looked to Elm, which also has global-only state and no real concept of components. Redux also puts most UI state in a single global store. Both of these demonstrated that real, full-scale apps can be written with mainly or exclusively global state, and in Elm's case, with no concept of components at all; only render-functions. The big difference, though, between these and Bagel is that you can skip all the message/command/reducer business and just mutate your state directly when it comes time to do that. Instead of a "UI component" you have a module that exports a render function, and event handlers that mutate state, and maybe some data types and some functions that construct instances of those types.

There's one other reason to have components, though: memoization/avoiding re-renders. But Bagel has this covered too; in fact, it dovetails wonderfully with its reactivity model.

Any function in Bagel can be marked as memo. With this, Bagel will memoize all of its return-values. Calling the same function with the same arguments will return a cached result instead of re-computing it. And, importantly, Bagel will invalidate the function's cached result whenever any of the arguments or any mutable state it captures in its closure is mutated. This is basically how MobX's computedFn works, and it's essential if we want to memoize over mutable data, which we do.

So, just memo your render function, and if the relevant state doesn't change between app renders, the previous render's output will be re-used.

I've written a simple GUI app using the above paradigm, and so far it works really nicely.

What's been solidified #

These are things that were only ideas, possibilities, or open questions last time, and have since congealed into realities or at least semi-firm plans:

Bites #

Ok, it's time. Let's look at some code! These will be some random samples that show off different aspects of the language.

Fibonacci #

Fibonacci sequence implemented in Bagel

We'll start with a classic. This file can be compiled directly into a bundle as the "main" file, and then run in either Node or Deno. A few things to note here:

Todo-list item "component" #

UI component implemented in Bagel

Bagel doesn't have React-style UI components, but this file represents the rough equivalent. Outside of this file you would have a store holding all application state and passing objects down to this render function, but all the details can still be described here, in one place.

LocalStorage integration #

Integration with local storage from Bagel

This is one of the integrations that's going to be shipped with the standard library. I chose this example because it demonstrates both a) what it looks like to set up observable state, and b) what JS-interop looks like.

Summary #

That was a lot! But then, a lot has happened. There's more that I could have talked about here, but I think I've covered the most important stuff. Bagel is real, it's coming along, and I'm excited about it.

There are still a couple of key pieces that need to be built before it can be used for anything real. Most crucially:

Once these are implemented, we may be getting close to a v0.1. Around that time I'll want to start building some more-than-toy projects with it, to sift out any glaring bugs or important missing features. This will probably include a nontrivial web app, and I may also start self-hosting Bagel's compiler.

If anybody finds themselves interested in contributing to and/or testing Bagel, feel free to email me at mail@brandons.me. I think right now would still be a bit early for contributing or testing, but I'd still love to talk about the project or receive suggestions/feedback.

Thanks for reading if you've made it this far! I'll post again when I have more updates.

Cheers