I'm just getting started with Nim, and really loving it. (My background is mostly C++ and Objective-C, but I've spent time in Java, Swift, Python, Ruby, Smalltalk-80, and I'm currently also learning Rust.)
I'd like to use a more Rust/Swift/etc. style where nil is not allowed. It looks like the not nil annotation and the options module will let me do that. But I'm running into problems (with Nim 1.2 on macOS). Yes, I realize that not-nil is experimental, but it's documented in the language manual so I'm assuming it's useable.
First, the compiler's nil-checker seems too limited. In the example below, it doesn't seem to realize that the raise never returns. Workaround is to put the return statement in an else: block.
{.experimental: "notnil".}
type
Foo = ref object
x: int
Container = object
foo: Foo not nil
proc newFoo(x: int): Foo =
if x < 0: nil else: Foo(x: x)
proc makeContainer(x: int): Container =
let f = newFoo(x)
if f == nil:
raise new CatchableError
return Container(foo: f) # Error: cannot prove 'f' is not nil
Mixing not-nil with Option produces warnings too:
import options
{.experimental: "notnil".}
type
FooObj = object
x: int
Foo = ref object not nil
proc makeFoo(f: Foo): Option[Foo] =
if f != nil:
return some(f)
else:
return none(Foo)
This produces:
notnil.nim(14, 20) template/generic instantiation of `some` from here
..../nim/1.2.0/nim/lib/pure/options.nim(119, 5) Warning: Cannot prove that 'result' is initialized. This will become a compile time error in the future. [ProveInit]
notnil.nim(16, 20) template/generic instantiation of `none` from here
..../nim/1.2.0/nim/lib/pure/options.nim(125, 3) Warning: Cannot prove that 'result' is initialized. This will become a compile time error in the future. [ProveInit]
Known issues? Are there workarounds?
You are not supposed to use not nil types as Option generic parameters, Options check if a type is ref or ptr or proc then assume their nil values to be None instead of using an extra bool discriminator, so some(nil) is also disallowed. You used Option[Foo] where Foo is not nil. There is also a convenience proc for if f != nil: some(f) else: none(Foo), option(f).
not nil is pretty weak and probably won't grow into a full stable feature. There are a couple open issues (this label is misleading, there's other nil errors in the closed issues) but they're very old.
There is an alternate proposal for nilability and nil types that might go into effect some time soon, and the Nim compiler is getting bindings to Microsoft's Z3 theorem prover engine under a new tool called "drnim". All I can say is you need to wait.
You are not supposed to use not nil types as Option generic parameters
Then how do I make a function that conditionally returns a value of that type, like the makeFoo function in my example? "Option(al)" is the canonical way to do this in other languages. I could use a regular ref as the parameter, like Option[ref FooObj], but then the value I extract from such an Option can't be assigned to a regular Foo since the compiler doesn't realize it can't be nil...
I can understand why the current implementation of the Option class might have trouble with a not-nil type, since the pointer it stores internally needs to support nil. That seems more like a bug/limitation of Option.
Sorry to hear that not nil is less capable than it looks (it really should be moved out of the main compiler docs), but I'm glad there's some recent design work going on. IMHO this is one of the weak corners of Nim as compared with other modern languages like Rust and Swift.
While everything that Hlaaftana said is true, there is no reason that
proc makeContainer(x: int): Container =
let f = newFoo(x)
if f == nil:
raise new CatchableError
return Container(foo: f)
has to work. It's unstructured control flow and our competitors don't support it either.
It's unstructured control flow and our competitors don't support it either.
Hm. Are you implying this is poor structure or non-idiomatic? Is it the lack of else: block you object to?
While I’m not sure whom you count as “competitors”, Clang does do that type of analysis. The control-flow analyzer would realize that f can’t be nil at the last line because the nil test dead-ends into a noreturn call. I’ve seen the static analyzer do much more advanced reasoning. (That said, compiling at -O0 without a static-analysis pass might not have the smarts to do this. I don’t normally build that way.)
I'd expect a {.noReturn.} proc or raise statement within a if foo == nil: branch to work.
Has this or will this be fixed?