Special values like NaN are half-assed sum types. The latter give you compiler guarantees.
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.
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.
That's fair, I wasn't dimsissing the practice but rather just commenting that it's a shame the author didn't clarify their preference.
I don't think the popularity angle is a good proxy for usefulness/correction of the practice. Many factors can influence popularity.
Performance is a very fair point, I don't know enough to understand the details but I could see it being a strong argument. It is counter intuitive to move forward with calculations known to be useless, but maybe the cost of checking all calculations for validity is larger than the savings of skipping early the invalid ones.
There is a catch though. Numpy and R are very oriented to calculation pipelines, which is a very different usecase to general programming, where the side effects of undetected 'corrupt' values can be more serious.
Anyway, this topic of "error handling scoping/locality" may be the single most cross-cutting topic across CPUs, PLangs, Databases, and operating systems (I would bin Numpy/R under Plangs+Databases as they are kind of "data languages"). Consequently, opinions can be very strong (often having this sense of "Everything hinges on this!") in all directions, but rarely take a "complete" view.
If you are interested in "fundamental, not just popularity" discussions, and it sounds like you are, I feel like the database community discussions are probably the most "refined/complete" in terms of trade-offs, but that could simply be my personal exposure, and DB people tend to ignore CPU SIMD because it's such a "recent" innovation (hahaha, Seymore Cray was doing it in the 1980s for the Cray-3 Vector SuperComputer). Anyway, just trying to help. That link to the DB Null page I gave is probably a good starting point.
If my grandmother had wheels, she'd be a bike.
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.
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.
tbh this system (assuming it works that way) would be more strict at compile-time than the vast majority of languages.
If you actually want the compiler to check this on the level of the type system, it'd have to be `NonNaNFloat | NaN`. Then you can check which one you have and continue with a float that is guaranteed to not be NaN.
But (importantly) a NonNaNFloat is not the same as a float, and this distinction has to be encoded in the type system if you want to take this approach seriously. This distinction is NOT supported by most type systems (including Rust's std afaik, fwiw). It's similar to Rust's NonZero family of types.
Hypothetically, no, the float type would not admit NaNs. You would be prevented from storing NaNs in them explicitly, and operations capable of producing NaNs would produce a `float | nan` type that is distinct from float, and can't be treated like float until it's checked for NaN.
And I'm not sure why it's being discussed as though this is some esoteric language feature. This is precisely the way non-nullable types work in languages like Kotlin and TypeScript. The underlying machine representation of the object is capable of containing null values, yes, but the compiler doesn't let you treat it as such (without certain workarounds).
Unfortunately, Rust doesn't seem to be smart enough to represent `Option<NotNan<f64>>` in 8 bytes, even though in theory it should be possible (it does the analogous thing with `Option<NonZero<u64>>`).
This thread is discussing the possibility of adding such an optimization: https://internals.rust-lang.org/t/add-float-types-with-niche...
An approach that I think would have most of the same correctness benefits as a proper sum type while being more ergonomic: Have two float types, one that can represent any float and one that can represent only finite floats. Floating-point operations return a finite float if all operands are of finite-float type, or an arbitrary float if any operand is of arbitrary-float type. If all operands are of finite-float type but the return value is infinity or NaN, the program panics or equivalent.
(A slightly more out-there extension of this idea: The finite-float type also can't represent negative zero. Any operation on finite-float-typed operands that would return negative zero returns positive zero instead. This means that finite floats obey the substitution property, and (as a minor added bonus) can be compared for equality by a simple bitwise comparison. It's possible that this idea is too weird, though, and there might be footguns in the case where you convert a finite float to an arbitrary one.)
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.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.
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.
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
import math
echo fcNormal
echo FloatClass.fcNormal
echo math.fcNormal
echo math.FloatClass.fcNormal
All of these ways of identifying the `fcNormal` enum value works, with varying levels of specificity.If instead you do `from math import nil` only the latter two work.
from strutils as su import nil
echo su.split "hi there"
(You can put some parens () in there if you like, but that compiles.) So, you can do Python-style terse renames of imports with forced qualification. You just won't be able to say "hi there".(su.split) or .`su.split` or the like.You can revive that, though, with a
template suSplit(x): untyped = su.split x
echo "hi there".suSplit`
That most Nim code you see will not do this is more a cultural/popularity thing that is kind of a copy-paste/survey of dev tastes thing. It's much like people using "np" as the ident in `import numpy as np`. I was doing this renaming import before it was even widely popular, but I used capital `N` for `numpy` and have had people freak out at me for such (and yet no one freaking out at Travis for not just calling it `np` in the first place).So, it matters a little more in that this impacts how you design/demo library code/lib symbol sets and so on, but it is less of a big deal than people make it out to be. This itself is much like people pretending they are arguing about "fundamental language things", when a great deal of what they actually argue about are "common practices" or conventions. Programming language designers have precious little control over such practices.
Not the best but there is precedent.
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.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 view both of these as not great. If you strictly want to rely on wraparound behavior, ideally you specify exactly how you're planning to wrap around in the code.
Edit: I guess they could get rid of a few numbers... Anyhow it isn't a philosophy that is going to get me to consider nimony for anything.
Not if you compile with optimizations on. This C code:
int wrapping_add_ints(int x, int y) {
return (int)((unsigned)x + (unsigned)y);
}
Compiles to this x86-64 assembly (with clang -O2): wrapping_add_ints:
lea eax, [rdi + rsi]
ret
Which, for those who aren't familiar with x86 assembly, is just the normal instruction for adding two numbers with wrapping semantics. 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 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.
Nim is Choice in many dimensions that other PLang's are insistently monosyllabic/stylistic about - gc or not or what kind, many kinds of spelling, new operator vs. overloaded old one, etc., etc., etc. Some people actually dislike choice because it allows others to choose differently and the ensuing entropy creates cognitive dissonance. Code formatters are maybe a good example of this? They may not phrase opposition as being "against choice" as explicitly as I am framing it, but I think the "My choices only, please!" sentiment is in there if they are self-aware.
Granted, I don’t know nim. Maybe you can’t define + and - operators for non numbers?
You could add a `SomeNumber` predicate to the `concept` to address that concern. `SomeNumber` is a built-in typeclass (well, in `system.nim` anyway, but there are ways to use the Nim compiler without that or do a `from system import nil` or etc.).
Unmentioned in the article is a very rare compiler/PLang superpower (available at least in Nim 1, Nim 2) - `compiles`. So, the below will print out two lines - "2\n1\n":
when compiles SomeNumber "hi": echo 1 else: echo 2
when compiles SomeNumber 1.0: echo 1 else: echo 2
Last I knew "concept refinement" for new-style concepts was still a work in progress. Anyway, I'm not sure what is the most elegant way to incorporate this extra constraint, but I think it's a mistake to think it is unincorporatable.To address your question about '+', you can define it for non-SomeNumber, but you can also define many new operators like `.+.` or `>>>` or whatever. So, it's up to your choice/judgement if the situation calls for `+` vs something else.
FWIW, the "catenation operator" in the Nim stdlib is ampersand `&`, not `+` which actually makes it better than most PLangs at visually disambiguating things like string (or other dynamic array, `seq[T]` in Nim) concatenation from arithmetic. So, `a&b` means `b` concatenated onto the end of `a` while `a+b` is the more usual commutative operation (i.e. same as `b+a`). Commutativity is not enforced by the basic dispatch on `+`, though such might be add-able as a compiler plugin.
Mostly, it's just a very flexible compiler / system.. like a static Lisp with a standard surface syntax closer to Python with a lot of parentheses made optional (but I think much more flexible and fluid than Python). Nim is far from perfect, but it makes programming feel like so much less boilerplate ceremony than most alternatives and also responds very well to speed/memory optimization effort.
However, the base case being 1 does not preclude other types than integers, as cb321 pointed out.
And FWIW there are many possible types for T, as small integer constants are compatible with many types. And because of the "proc `<=`(a, b: Self): bool" in the concept definition of Fibable, the compiler knows that "2" is a constant of type T ... so any type that has a conversion proc for literals (remember that Nim has extensive compile-time metaprogramming features) can produce a value of its type given "2".
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".
Waiter: "How is everything?"
Customer: "Great!"
Waiter, disgusted: "Even war?"
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.
I wonder how Nim 3/Nimony handles or will handle bindings in patterns regarding copy, move or reference. Rust can change it per binding, and Ada's experimental pattern matching might have some plans or properties regarding that.[1]
> By default, identifier patterns bind a variable to a copy of or move from the matched value depending on whether the matched value implements Copy.
> This can be changed to bind to a reference by using the ref keyword, or to a mutable reference using ref mut. For example:
match a {
None => (),
Some(value) => (),
}
match a {
None => (),
Some(ref value) => (),
}
.The Github issue had a strange discussion. I really disliked goteguru's equals-sign-based syntax, though I had difficulty judging the main design syntax.
I wonder what Araq thinks of Scala's Expression AST type. Tree, TermTree, and all the subtype case classes [2]. Tree has fields. Though I am not certain how the common variables are initialized.
[1] https://doc.rust-lang.org/reference/patterns.html#r-patterns...
[2] https://github.com/scala/scala3/blob/main/compiler/src/dotty...
Good luck. Give the avionics guys a call if you solve this at the language level.
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.