The first Nimony progress thread became too long to. This thread is the 2nd progress report for Nimony.
Recently this program compiled (and produced the correct output):
import std / [syncio]
type
BinaryTree[T] = ref object
le, ri: nil BinaryTree[T]
data: T
proc newNode*[T](data: sink T): BinaryTree[T] = BinaryTree[T](data: data)
proc append*[Ty: Comparable](root: var nil BinaryTree[Ty], n: BinaryTree[Ty]) =
# insert a node into the tree
if root == nil:
root = n
else:
var it = root
while it != nil:
var c = cmp(n.data, it.data)
if c < 0:
if it.le == nil:
it.le = n
return
it = it.le
else:
if it.ri == nil:
it.ri = n
return
it = it.ri
proc append*[Ty](root: var BinaryTree[Ty], data: sink Ty) =
append(root, newNode(data))
type
Stringable = concept
proc `$`(x: Self): string
proc toString[T: Stringable](n: nil BinaryTree[T]; result: var string) =
if n == nil: return
result.add $n.data
toString n.le, result
toString n.ri, result
proc `$`*[T](n: BinaryTree[T]): string =
result = ""
toString n, result
proc main =
var x = newNode("abc")
x.append "def"
echo $x
main()
Feel free to continue the discussion about what syntax to use for nil ref T, ref T not nil, unchecked ref T here.
var x: ref Obj of Nilable
var y: ref Obj of NonNil
Neat to see the progress :-)
Regarding the not nil syntax, I like "not nil ref T", but I don't like "nil ref T" as much. To me "nil ref T" reads as "this is nil", rather than "this may be nil". It's not a big issue, but if we are looking for an alternative, what about using "not nil" for refs that cannot be nil, but use something else scuh as "maybe" or "nillable" for those that can? That is:
var x: not nil ref RootObj # Non-nil
var y1: nillable ref RootObj # Nillable option 1
var y2: maybe ref RootObj # Nillable option 2
this has the benefit of making them quite different, making them easy to distinguish. Of course some might think that them being so different is a negative rather than a positive. It also has the negative of requiring the addition of a new keyword.le, ri: nil BinaryTree[T]
Does that mean nimony will default to not nil? In such case I'd love BinaryTree[T] or nil syntax to opt out. It's syntactically similar to existing BinrayTree[T] not nil, which I find pretty natural. I'd also appreciate some global flag to disable not-nil-by-default behavior for at least a year, so that we have time to migrate. Otherwise I'm very excited about this (cough breaking) change! :)
I think it would be nice to have a clearer visual distinction between the different kinds of references, since it's a lot of “fun” to read the full type descriptor to see what it really is (especially since they are currently in opposite sides of each other). As an example, you could use the good old ! and ? symbols after the ref keyword. This would be grammatically simple, as well as more visually clear.
For example:
type UncheckedRef[T] = ref T
type NillableRef[T] = ref? T # instead of `nil ref T`
type NotNilRef[T] = ref! T # instead of `ref T not nil`
But I understand that this may be unwanted, since elsewhere the language almost never uses any symbols for such things.
Does that mean nimony will default to not nil?
Yes but the plan is to have a switch like {.refs: unchecked|notnil.} for a migration period.
In such case I'd love BinaryTree[T] or nil syntax to opt out. It's syntactically similar to existing BinrayTree[T] not nil, which I find pretty natural.
I find or nil too similar to not nil. When skimming code this is quite unclear IMO.
I like the or nil version, maybe this looks a bit more distinct:
type NillableRef[T] = ref T or nil
type NotNilRef[T] = ref T is val
And a couple more variants:
type NillableRef[T] = nilref T # short for Nillable Reference
type NotNilRef[T] = valref T # short for Value Reference (always points to value)
type NillableRef[T] = opt ref T
type NotNilRef[T] = val ref T
I like the simplicity of nil not nil prefix. @Saffage version looks very readable too :
type UncheckedRef[T] = ref T
type NillableRef[T] = ref? T # instead of `nil ref T`
type NotNilRef[T] = ref! T # instead of `ref T not nil`
I propose creating a new option concept with a single character ? called "maybe". In the definition, it serves as an Option[T] or nil ref. In usage, ? serves as a boolean check to see if the value is present.
The ref becomes non-nil always. This means ref must always point to a value-just like int or any other type is required to have a value. And ref? becomes a nullable or optional-or what I call a "maybe"-ref that can be absent. In the same way, int? becomes an optional or "maybe" int that can be absent. It's very symmetrical. Optionals and nullability are very similar concepts. Fewer concepts are better.
The "maybe" ? also becomes a postfix operator that checks if the value is present in both the ref? and int? cases. It's the same concept now:
proc getInteger(): int?
let value = getInteger()
if value?:
echo "Value: ", value
else:
echo "No value"
proc getObject(): ref? BigObject
let obj = getObject()
if obj?:
echo "Object: ", obj
else:
echo "No object"
This does mean that ref or even ref? can't be compared to nil anymore. You must use if ref? or if not ref? to check if the value is present. The concept of a null/nil ref is gone. Only pointer or ptr retains the nil concept.
The compiler should check any access to ref? or int? and produce an error if the value has not been checked with if or if not earlier in that block. This will prevent many bugs.
Making something like ? into a broader concept seems much more elegant than as a one off syntax. Maybe appealing enough to justify adding a pervasive sigil like that?
Though thinking of it more like {.requiresInit.} or like var would be better than as an explicit option type. It’s sorta like a form of dependent typing instead and doesn’t require mucking with the original type. The compiler could use flags or whatever mechanism to efficiently implement any runtime checks. It’d be similar to how quirky exceptions work if I understand them correctly.
For example what if int? was just an int, but checked that it was set or initialized. If a user wanted to store it in a type they’d have to wrap it themselves into an Option[T] or Result or whatever the type the wanted (or ignore it and set a default value). Personally I dislike pervasive option types as they add overhead and have a viral tendency.
That’d additionally avoid a whole slew of issues in what optional types could be used and how to specify them. The compiler would enforce that conversion naturally:
type Foo = object
val: Option[int]
var foo: Foo
If value?:
foo.val = some(value)
else:
foo.val = int.none
#default:
var x: nil ref RootObj #reading: nilable
# later obsolet, use only pragmas
var x: ref RootObj {.notnil.}
var x: ref RootObj {.unchkd.}
or
#default:
var x: nil ref RootObj
# later obsolet - as keywords consistent on the left
# a)
var x: not nil ref RootObj
var x: not chk ref RootObj
# b)
var x: notnil ref RootObj
var x: unchkd ref RootObj
FWIW, "compiler/PLang people" always say things like "nilable" (typically with 2 'l' - i.e. not "nillable", no pun intended), but "maybe" works much better with "ordinary English" and I was going to suggest that myself. It suggests possible ignorance and the way you convert "maybe" into "definitely" is with a check.
I also agree with @treeform here that the broader idea of need-to-check is common enough to deserve first class, terse support.
There are entire schools of identifier debates, but in my experience the Scheme-style predicate? is more popular than the seemingly pre-ASCII Lisp predicateP or predicateQ. Also, I really don't understand how a single ? can be viewed as "heavyweight" compared to all these keywords. I would have anticipated "too terse" as the objection.
If ? is deemed not explicit enough then considering ref == not nil ref ` by default and adding `nil ref T when needed is what makes the most sense and seems the most elegant.
And then we can refine in std/options or / and add ? notation in external library on top of this