The First Complete Shape of File Handling in Luma
When a piece of a language reaches its first complete shape, it’s rarely because everything imaginable has been added. More often, it’s because the unnecessary parts have been taken away. That’s where file handling in Luma is today.
This first wave is not about finishing every possible feature. It’s about reaching a point where the model is coherent, predictable, and aligned with the way the language wants you to think. You can now work with files comfortably, without ceremony, and without constantly being reminded of lower-level mechanics that don’t belong in everyday code.
From “doing something” to “referring to something”
One of the earliest design mistakes we made was subtle: we assumed that creating a file value should immediately interact with the filesystem. That assumption brought along all kinds of awkward behavior. Files that didn’t exist yet caused failures too early. Simple flows like “create a file if it’s missing, then write to it” required mental gymnastics or defensive checks that felt out of place.
The fix was not technical — it was conceptual.
In Luma today, creating a file value means exactly one thing: you are naming a file.
f: file = file("app.log")This line does not open the file. It does not check whether it exists. It does not fail. It simply creates a value that represents a path. That may sound trivial, but it completely changes how natural file-related code feels. You can now talk about files that don’t exist yet, pass them around freely, and decide later what to do with them.
Once this distinction was made, the rest of the API fell into place almost on its own.
Operations that do exactly what they say
With files treated as references, file operations could become honest and self-contained. Each method now does one thing, performs it immediately, and cleans up after itself. There is no persistent handle, no implicit state, and no hidden lifetime to worry about.
f: file = file("notes.txt")
f.write("First line")
f.add("Second line")
f.add("Third line")Every call opens the file, performs the operation, and closes it again. This matches the way most programs actually use files: small, clear actions that don’t require long-lived resources. It also makes error behavior obvious. If something goes wrong, you know exactly which operation caused it.
The result is an API that feels boring in the best possible way.
Letting types express intent
One of the strongest ideas in Luma is that types are not just there for checking — they describe intent. File reading is where this idea becomes most visible.
There is only one method for reading files:
read()What it does depends entirely on the type you assign the result to.
content: str = file("config.txt").read()
lines: [str] = file("data.txt").read()
bytes: [byte] = file("image.png").read()The method name doesn’t change. You don’t have to remember a family of specialized functions. The code tells you what it expects, and the language follows that expectation.
The same applies to error handling. If a file is required, you say so by using a non-optional type. If it’s optional, you express that explicitly.
config: ?str = file("app.config").read()
if config != nil {
print("Config loaded")
} else {
print("Using defaults")
}This removes a surprising amount of noise. There’s no need for tryRead, no flags, no special naming conventions. One method, multiple meanings, all visible at the type level.
Writing that mirrors how people think
Writing to files follows the same philosophy. You don’t tell the language how to write — you tell it what you’re writing.
file("out.txt").write("Hello, world")If you pass text, text is written. If you pass bytes, bytes are written. The method adapts naturally to the data.
Appending deserves special mention, because this is where design restraint mattered. Instead of adding flags or parameters, Luma separates the common case from the rare one.
log: file = file("app.log")
log.add("Application started")
log.add("User logged in")add means “this is a new entry”. A newline is part of that meaning. When you need raw control, you opt into it deliberately:
log.append("[CONTINUED] ")
log.add("Recovered after restart")This keeps everyday code readable while still allowing precision when it’s truly needed.
Why some ideas were removed
Reaching this point also meant letting go of ideas that initially seemed useful. Streaming file handles, explicit open/close blocks, and cursor-style line reading were all explored. They worked — but they didn’t earn their place.
Each of those features introduced new syntax, new rules, and new mental models. In return, they solved problems that already had simple, composable answers. They made the language larger without making everyday code clearer.
Instead of a dedicated streaming API, the current approach favors simplicity and composability:
lines: [str] = file("data.txt").read()
lines.walk(i, line) -> {
print(line)
}This works well in practice, keeps file handling aligned with the rest of the language, and avoids turning file I/O into a special case that needs to be learned separately. For truly massive data sets, system tools or specialized pipelines are still the right choice — and that’s okay.
Removing these features wasn’t about limiting power. It was about protecting clarity.
Working with many files, naturally
File handling wouldn’t be complete without a way to work with groups of files. Pattern matching fits naturally into the same model.
logs: [file] = glob("logs/*.log")
logs.walk(i, log) -> {
print(log.name())
}Patterns never throw errors. No matches simply mean an empty list. This keeps the flow predictable and easy to compose with other parts of the language.
What “complete” means here
Calling this API complete doesn’t mean it will never grow. It means something more important: the foundation is stable. The concepts are consistent. The removed ideas are unlikely to return, because the language no longer needs them.
Future additions — such as file timestamps — will fit into this model rather than bending it.
This first wave didn’t arrive by adding more and more features. It arrived by stepping back, questioning assumptions, and choosing simplicity over cleverness.
File handling in Luma now feels like it belongs.
And that’s the kind of progress worth announcing.