Nimony (Nim 3.0) Design Principles

4 days ago (nim-lang.org)

> "Modern" languages try to avoid exceptions by using sum types and pattern matching plus lots of sugar to make this bearable. I personally dislike both exceptions and its emulation via sum types. ... I personally prefer to make the error state part of the objects: Streams can be in an error state, floats can be NaN and integers should be low(int) if they are invalid.

Special values like NaN are half-assed sum types. The latter give you compiler guarantees.

  • I’d like to see their argument for it. I see no help in pushing NaN as a number through a code path corrupting all operations it is part of, and the same is true for the others.

    • The reason NaN exists is for performance AFAIK. i.e. on a GPU you can't really have exceptions. You don't want to be constantly checking "did this individual floating-point op produce an error?" It's easier and faster for the individual floating point unit to flag the output as a NaN. Obviously NaNs long predate GPUs, but floating-point support was also hardware accelerated in a variety of ways for a long time.

      That being said, I agree that the way NaNs propagate is messy. You can end up only finding out that there was an error much later during the program's execution and then it can be tricky to find out where it came from.

    • There is no direct argument/guidence that I saw for "when to use them", but masked arrays { https://numpy.org/doc/stable/reference/maskedarray.html } (an alternative to sentinels in array processing sub-languages) have been in NumPy (following its antecedents) from its start. I'm guessing you could do a code-search for its imports and find arguments pro & con in various places surrounding that.

      From memory, I have heard "infecting all downstream" as both "a feature" and "a problem". Experience with numpy programs did lead to sentinels in the https://github.com/c-blake/nio Nim package, though.

      Another way to try to investigate popularity here is to see how much code uses signaling NaN vs. quiet NaN and/or arguments pro/con those things / floating point exceptions in general.

      I imagine all of it comes down to questions of how locally can/should code be forced to confront problems, much like arguments about try/except/catch kinds of exception handling systems vs. other alternatives. In the age of SIMD there can be performance angles to these questions and essentially "batching factors" for error handling that relate to all the other batching factors going on.

      Today's version of this wiki page also includes a discussion of Integer Nan: https://en.wikipedia.org/wiki/NaN . It notes that the R language uses the minimal signed value (i.e. 0x80000000) of integers for NA.

      There is also the whole database NULL question: https://en.wikipedia.org/wiki/Null_(SQL)

      To be clear, I am not taking some specific position, but I think all these topics inform answers to your question. I think it's something with trade-offs that people have a tendency to over-simplify based on a limited view.

      2 replies →

  • The compiler can still enforce checks, such as with nil checks for pointers.

    In my opinion it’s overall cleaner if the compiler handles enforcing it when it can. Something like “ensure variable is initialized” can just be another compiler check.

    Combined with an effects system that lets you control which errors to enforce checking on or not. Nim has a nice `forbids: IOException` that lets users do that.

    • Both of these things respectively are just pattern matches and monads, just not user-definable ones.

    • > The compiler can still enforce checks, such as with nil checks for pointers.

      Only sometimes, when the compiler happens to be able to understand the code fully enough. With sum types it can be enforced all the time, and bypassed when the programmer explicitly wants it to be.

      1 reply →

The biggest thing I still don’t like about Nim is its imports:

    import std/errorcodes

    proc p(x: int) {.raises.} =
      if x < 0:
        raise ErrorCode.RangeError
      use x

I can’t stand that there’s no direct connection between the thing you import and the names that wind up in your namespace.

  • There is a direct connection, you just don't have to bother with typing it. Same as type inference, the types are still there, you just don't have to specify them. If you have a collision in name and declaration then the compiler requires you to specify which version you wanted. And with language inspection tools (like LSP or other editor integration) you can easily figure out where something comes from if you need to. Most of the time though I find it fairly obvious when programming in Nim where something comes from, in your example it's trivial to see that the error code comes from the errorcodes module.

    Oh, and as someone else pointed out you can also just `from std/errorcodes import nil` and then you _have_ to specify where things come from.

    • When I was learning Nim and learned how imports work and that things stringify with a $ function that comes along with their types (since everything is splat imported) and $ is massively overloaded I went "oh that all makes sense and works together". The LSP can help figure it out. It still feels like it's in bad taste.

      It's similar to how Ruby (which also has "unstructured" imports) and Python are similar in a lot of ways yet make many opposite choices. I think a lot of Ruby's choices are "wrong" even though they fit together within the language.

  • It needs to be this way so that UFCS works properly. Imagine if instead of "a,b".split(','), you had to write "a,b".(strutils.split)(',').

    • ok I do not understand.

      What is preventing this import std/errorcodes

      from allowing me to use: raise errorcodes.RangeError instead of what Nim has?

      or even why not even "import std/ErrorCodes" and having the plural in ErrorCodes.RangeError I wouldn't mind

      1 reply →

  • Nim imports are great. I would hate to qualify everything. It feels so bureaucratic when going back to other languages. They never cause me issues and largely transparent. Best feature.

  • You are free to import nil and type the fully qualified name.

    • There are many things to like about Nim, but it does benefit from adherence to a style guide more than most languages.

Big "college freshman" energy in this take:

  I personally prefer to make the error state part of the objects: Streams can be in an error state, floats can be NaN and integers should be low(int) if they are invalid (low(int) is a pointless value anyway as it has no positive equivalent).

It's fine to pick sentinel values for errors in context, but describing 0x80000000 as "pointless" in general with such a weak justification doesn't inspire confidence.

  • Without the low int the even/odd theorem falls apart for wrap around I've definitely seen algorithms that rely upon that.

    I would agree, whether error values are in or out of band is pretty context dependent such as whether you answered a homework question wrong, or your dog ate it. One is not a condition that can be graded.

  • I have been burned by sentinel values every time. Give me sum types instead. And while I’m piling on, this example makes no sense to me:

        proc fib[T: Fibable](a: T): T =
          if a <= 2:
            result = 1
          else:
            result = fib(a-1) + fib(a-2)
    

    Integer is the only possible type for T in this implementation, so what was the point of defining Fibable?

    • I agree about sentinel values. Just return an error value.

      I think the fib example is actually cool though. Integers are not the only possible domain. Everything that supports <=, +, and - is. Could be int, float, a vector/matrix, or even some weird custom type (providing that Nim has operator overloading, which it seems to).

      May not make much sense to use anything other than int in this case, but it is just a toy example. I like the idea in general.

      5 replies →

    • There can be a lot of different integers, int16, int32 ... and unsigned variants. Even huge BigNum integers of any lengths.

From my interaction with the Nim community, I came to the conclusion that nim could be more popular if its founder devolved decision making to scale up the community. I think he likes it the way it is; small, but his. He is Torvaldsesque in his social interactions.

  • I feel the same way - as I suspect a lot of people here do. Nim posts are always upvoted and usually people say nice things about the language in the comments.. but there are few who claim to actually -use- the language for more than a small private project, if even that.

    • The only way to really test out a programming language is by trying it out or reading how someone else approached a problem that you're interested in/know about.

      There are over 2200 nimble packages now. Maybe not an eye-popping number, but there's still a good chance that somewhere in the json at https://github.com/nim-lang/packages you will find something interesting. There is also RosettaCode.org which has a lot of Nim example code.

      This, of course, does not speak to the main point of this subthread about the founder but just to some "side ideas".

  • I worked in nim for a little bit and it truly has a lot of potential but ultimately abandoned it for the same reason. It's never going to grow beyond the founder's playground.

> floats can be NaN and integers should be low(int) if they are invalid (low(int) is a pointless value anyway as it has no positive equivalent).

I have long thought that we need a NaI (not an integer) value for our signed ints. Ideally, the CPU would have overflow-aware instructions similar to floats that return this value on overflow and cost the same as wrapping addition/multiplication/etc.

  • From an implementation point of view, it would be similar to NaN; a designated sentinel value that all the arithmetic operations are made aware of and have special rules around producing and consuming.

> It is not possible to say which exceptions are possible

So repeating the same mistake that Spring made by using runtime exceptions everywhere.

Now you can never know how exactly a function can fail, which means you are flying completely blind.

>WCET ("worst case execution time") is an important consideration: Operations should take a fixed amount of time and the produced machine code should be predictable.

Good luck. Give the avionics guys a call if you solve this at the language level.