type
Either*[L, R] = object
case isRight*: bool # true = Right (success), false = Left (error)
of true:
right*: R # successful value
of false:
left*: L # error value (can be string, ref Exception, custom error type...)
# Helper constructor procedures (like Just/Nothing or Right/Left)
proc left*[L, R](value: L): Either[L, R] =
Either[L, R](isRight: false, left: value)
proc right*[L, R](value: R): Either[L, R] =
Either[L, R](isRight: true, right: value)
Exceptions in programming languages are like making a mess in a moving train and having the conductor throw you out while it's still going. In a multithreaded program, exceptions are like being thrown out on a multi-track railway — you fall under another train and bring the entire station to a halt.
The Either data type solves this simply: whoever makes a mess goes left, and whoever behaves properly goes right. Then you can calmly decide what to do next.
Thanks to Either, I was able to handle errors beautifully in my multi-threaded file downloader. Here is this file.
The code is quite nice — even an AI praised me for it. It gave me a lot of advice, but it's pure human handiwork. Without my own thinking, the AI couldn't have done it. I thank it for the suggestions, but I often had to correct it myself. :-)
However, the code is part of a larger project that is a mix of many ideas. It is still intensively developing, constantly changing, there are no guarantees of backward compatibility, and for now it’s more of a hobby project. Besides, English is not my favorite language — I don’t master it very well, and I don’t particularly like it either. Many comments and names are therefore in my native language. I know this isn’t the best way to reach the wider world, but it’s my own stubborn path. :-)
In this file, you can see how I used Either to return errors. On success, I return just the file path right path:Path on failure, I return left (error_message: String, the_path_that_was_not_saved: Path). I think it's a pretty nice solution. Can I be proud of myself? :-)
Back to the Either type. I think it's a great idea, but one thing bothers me: Nim doesn't infer the generic type parameters even when it should be able to.
proc f() : Either[string, Path] =
let file = "example.nim".Path
return right(file) # error
return right[string, Path](file) # works
This is a bit annoying and ugly — especially with more complex types like Either[(string, Path), Path]. The AI claims that it should work without explicit type parameters, but sometimes it just doesn't. Is there any chance it will work automatically in the future? I understand this might be a complicated issue.
Thank you and have a nice day!
Živoslav
Checkout https://github.com/arnetheduck/nim-results/ if you’d like a prebuilt one that’s used a lot already.
The issue with the return type is a known issue in many cases. It’s one of many reasons I personally dislike result types.
proc left*[L, R](E: typedesc[Either[L, R]], value: L): Either[L, R] =
Either[L, R](isRight: false, left: value)
proc right*[L, R](E: typedesc[Either[L, R]], value: R): Either[L, R] =
Either[L, R](isRight: true, right: value)
proc set*[L, R](dst: var Either[L, R], value: L) =
dst = left[L, R](value)
proc set*[L, R](dst: var Either[L, R], value: R) =
dst = right[L, R](value)
template returnLeft*(x: typed): untyped =
return left(typeof(result), x)
template returnRight*(x: typed): untyped =
return right(typeof(result), x)
import std/paths
proc foo(): Either[string, Path] =
let file = "example.nim".Path
result.set file
proc bar(): Either[string, Path] =
let file = "example.nim".Path
returnRight file
echo foo()
echo bar()
Doesn't seem to be such an inconvenience. But I guess it would be nice to be able to use return destination type as a hint.
Thanks.
I quite like the solution with the template, but it doesn't work for me.
Error: type mismatch
Expression: right(typeof(result), file)
[1] typeof(result): typedesc[Either[system.string, paths.Path]]
[2] file: Path
Expected one of (first mismatch at [position]):
[1] proc right[L, R](value: R): Either[L, R]
The solution with set has one drawback. As soon as I use Either[string, string], we get an ambiguous call.
return left "any string" triggers Error: cannot instantiate: 'R'
only return left[string, string] "any string" — with the types explicitly specified — compiles without error.
I really like this Either — it's incredibly simple yet extremely flexible. Having to specify the data types when calling left and right is a small extra bit of work, but in return the code becomes a little clearer.
I'll try to use this "functional paradigm" more often instead of raise Exception. What bothers me about exceptions is that wherever I later handle the error, I only have a textual string available (err.msg), whereas with Either I can propagate anything I want along with the error. I use it in my downloader, I used Either[(string, Path), Path], so on failure I not only have a description of what went wrong, but I also have some variable associated with it that I can do something with — even if I just send it to the log, it's worth it.
Thanks. That module has 1,649 lines of code, plus a benchmark module and two test modules.
If it’s supposed to serve the same purpose as my Either, then it’s far too complicated. There’s probably something more going on there that I’m missing — and that I most likely won’t make use of at this point.
You probably forgot these helpers:
proc left*[L, R](E: typedesc[Either[L, R]], value: L): Either[L, R] =
Either[L, R](isRight: false, left: value)
proc right*[L, R](E: typedesc[Either[L, R]], value: R): Either[L, R] =
Either[L, R](isRight: true, right: value) If it’s supposed to serve the same purpose as my Either, then it’s far too complicated.
Yeah it’s the same purpose as your Either type.
That module is mostly helpers like flatMap, map, mapOr etc, from the functional world. I presume it has benchmarks because for large projects a Result/Either type can add performance overhead in some cases.
There’s probably something more going on there that I’m missing — and that I most likely won’t make use of at this point.
The core type is almost identical to yours, but handles corner cases with like void types. Though yes it has lots of helpers.
I’ve got no skin in the game, but you could likely learn some tricks reading through it. I believe there’s comments / issues discussing the need for generics on returns, etc.
Happy coding!
For the particular case of deducing return types, nim-results uses the same trick that darkestpigeon suggests.
This doesn't generalize to variable assignments however: https://github.com/arnetheduck/nim-results/issues/28.
There's a trick in that issue using an intermediate type which could maybe work, though it hasn't been explored fully yet - in particular, it's easy to make something that looks like it works for simple cases but breaks down under generics or runs into some obscure corner case of the language. Certainly high on the wishlist to improve and if you make progress, I'd be happy to know so it can be backported to results.
This is also what the test cases are for: to ensure that the type (and all its workarounds for past and present Nim bugs) integrates well with the rest of the language.
module has 1,649 lines of code..something more going on there that I’m missing
Probably you're missing that most of it is documentation as well has handling of special cases like void and integrations with the rest of the language, like for loops and utilities to lift the values and errors to perform operations on them (like map etc)
In languages like Haskell, the language itself provides many of these utilities or they are not needed because the code using Either solves these problems in a different way.
Regarding benchmarks, this is another point you probably haven't considered yet: Nim liberally introduces temporary copies of values in the generated code, often without it being obvious - this is one of the biggest sources of inefficiency when using a type like Either in an API - for example, returning Either[string, string] is a good way of introducing 3-4 extra copies of the string you're returning if you're not careful in your implementation of Either and even if the implementation is well done, there are and inefficiencies in the code that the Nim compiler emits that makes the overhead higher than it needs to be.
While this overhead is negligible in simple usage it adds up over time, specially in a larger codebase that uses Either (or really Result as it happens to be spelled in most projects using an Either-like type in Nim).
For this reason, there are benchmarks of Result - not only to understand where its inefficiencies lie but also to discover where the compiler emits poor code and eventually address those.
If you want to apply your idea of avoiding exceptions in a larger codebase, you'll have to be a bit more careful when working with "big" value types like array, seq and string, measuring your hotspots. This is what we do for most Nim code at Status since code using exceptions by far has more bugs and leaks, with the tradeoff of having to do more work to maintain performance in certain cases.
Thank you. So is nim-results the recommended and maintained solution for the “functional approach”? I’d be happy to use it. I’m gradually discovering these low‑level things; originally I programmed in Python, then I realized the usefulness of static typing, then I started experimenting with things like Cython and other Python→C compilers, and that’s when I discovered Nim. I was excited, and I’m still excited — I’ve been using Nim for 7 years. But there are many libraries that aren’t very good or that stopped being maintained, though that happens in other ecosystems as well. Nim is great because it’s easy to interface with any C library at any time, or even with Python libraries thanks to the excellent nimpy, which opens up the wealth of libraries from those languages.
Originally I saw Nim as an ideal and safe way to replace the sometimes unpredictable Python and to make working with C easier (pointers and those low‑level things were always a bit difficult for me). Then I realized that memory management and the garbage collector are also areas where Nim still requires a lot of work, and that people are still searching for the ideal solution.
Can I treat nim-results as a good standard for development? I found a clear but extensive write‑up about it here: https://status-im.github.io/nim-style-guide/errors.result.html
I’m working on a project that has similar social goals to Status :-), and Nim is a practical tool for me. I also tried using the Status application as a regular user and wanted to build something on top of it, to deploy it for our community, but so far I haven’t succeeded. And from a programmer’s perspective, the codebase is quite complex and difficult to learn. We’re happy that people are starting to use Signal more than WhatsApp. I had a plan to deploy Status‑im as a federated client so people could stay on matrix‑like networks (there seems to be some issue with Signal integration now), but I still haven’t managed to really get into Status‑im. I don’t think I even managed to compile it — the AppImage is the only thing that actually works. And that’s not a path I like; I prefer more open packaging, the ability to install from Debian or Arch repositories. I see that at least a Nix package is available. I really appreciate your work — I just want to give feedback that getting started with Status‑im wasn’t very easy.
Can I treat nim-results as a good standard for development? I found a clear but extensive write‑up about it here: https://status-im.github.io/nim-style-guide/errors.result.html
We certainly use it productively for a category of code that lends itself well to a functional approach and find it easier to reason about, which ultimately leads to fewer bugs. Code written to use nim-results is more likely to be correct and stay correct during maintenance.
In the category of "either-like" constructs, what probably stands out is not nim-results itself but rather the number of libraries that use or expose it. If anything, nim-results itself is rather conservative, ie it does not add "modern" features (like sink) until they become stable in the compiler since doing so would break a lot of downstream code. It doesn't change a lot because most of it "just works".
Some projects take it further with additional "magic syntax" like https://github.com/logos-storage/questionable but that's certainly more on the experimental side of things and the uptake has been rather limited - the same goes for most other fancy-syntax libraries that offer pattern matching etc - people write these kinds of libraries regularly but what we're all really waiting for is language support to make it ergonomic and free from edge cases.
Once 559 gets implemented, libraries like nim-results will be drastically simpler to implement (and use), and above all, it's a building block for making progress on the "infer generic parameter" in a principled way that has staying power.
Our experience is that code using exceptions or "side channels" is easier to write sometimes at the expense of having to do more maintenance later on, as users find bugs in it - typically it's easier to write because you simply skip the parts that can go wrong and wait for it to crash or fail before you deal with them which tends to lead to longer development cycles over time to reach a stable solution - but of course, it's not all about exceptions or results - you can write both good and bad code with both.
Where we don't use nim-results, we use {.push raises: [].} which is a weaker version of the same thing that makes nim-results-based code less buggy: error handling at the call site enforced up front by the compiler.
Nim is a practical tool for me
It is for us too, hence nim-results ;)
Status‑im
It is indeed a complex product for lots of reasons and not all of them good or current - it has slowly been getting better though as the codebase was unified for android, ios and desktop, but it's also a product that has a lot of auxiliary features like hardware crypto wallet support which in turn means a more complex and fragile build.
Building it from within the nix environment is probably the more stable and predictable way to go but it depends a lot on what versions of libraries you have installed on system - if you have problems, do open issues in the tracker or reach out to the devs - they'll be happy to take a look.