Reposted from Reddit:
Hi, thanks for answering my questions for a week, while I studied Nim.
I wrote the second version of my feedback about using Nim http://al6x.com/blog/2020/nim-language
I wrote it for myself as a note to check Nim against it in a couple of years, as I believe Nim has potential.
Right now though, at least for my use cases it feels too immature and won't be able to replace Kotlin and TS.
I suggest us to discuss points from this article :)
Some of my comments:
"The standard library Nim documentation suggest us to generate JSON by hand with JsonNode . And we absolutelly not going to do that, nobody generates JSON by hand these days. Not good, when official docs promoting inconvenient way to do things."
Marshal shouldn't be used for JSON serialization at all, and it's not advertised as such! It's just for serialization and happens to use JSON, and it's deprecated anyway. We actually have json.to -> https://nim-lang.org/docs/json.html#to%2CJsonNode%2Ctypedesc%5BT%5D.
"There are two problems, first - simple echo all_docswon't work without reprand the second - the output is not clean, it's polluted with the refs noise we don't care about."
You have to dereference a ref to echo it: echo all_docs[]
Hmm, I understand what you are saying, but seems like people still use marshal for JSON conversion, and this StackOverflow question has the most votes on using marhsal :)
https://stackoverflow.com/questions/26191114/how-to-convert-object-to-json-in-nim
Also, about the json - I may be missing something, but it proposes to manually generate JSON. That's not a good option, JSON should be generated automatically.
You may need some tools to alter the default behaviour of Object to JSON conversion and fine-tune it, but the default behaviour should be good enough to work in most cases.
It does not propose to generate JSON manually:
import json
type
Person = object
name: string
age: Natural
let a = Person(name: "Someone", age: 18)
echo %*a
Please see my comment on that stackoverflow answer as well as your thread on this forum.
That Stackoverflow answer is 6 years old at this point, things change. Although I would say that even then it wasn't a correct answer. Please downvote it accordingly.
What about the object variant safety issue?
type
DocKind = enum
text,
todo
Doc = object
case kind: DocKind
of text:
text: string
of todo:
todo: string
let doc = Doc(kind: text, text: "some doc")
echo doc.todo#compiles, throws exception
As far as I can tell, preventing this generally would require forbidding access of variant fields when not inside a case kind typeguard.
Would that tradeoff between flexibility and safety be desirable?
First of all thanks to OP redditor and everyone else for this thread. I have been curious about Nim for a long time and interested in its balance of great features, nice syntax and maybe some rough edges.
Please forgive my completely uninformed participation, as someone entirely new to Nim, but I also wanted to understand a bit how it works (I previously asked a related question on Reddit a few months ago and the answer was basically "use marshal").
My impression is that Nim behaviour here seems a bit inconsistent...
import json
type
DocItem = object of RootObj
tags: seq[string]
TextDoc = ref object of DocItem
text: string
TodoDoc = ref object of DocItem
todo: string
var docs: seq[ref DocItem]
let textdoc = TextDoc(text: "some doc", tags: @["help"])
let tododoc = TodoDoc(todo: "some todo")
docs.add textdoc
docs.add tododoc
echo textdoc[]
# (text: "some doc", tags: @["help"])
echo textdoc.text
# some doc
echo tododoc[]
# (todo: "some todo", tags: @[])
echo tododoc.todo
# some todo
# ...all good so far
echo docs[0] of TextDoc
# true
echo docs[0][]
# (tags: @["help"])
# 🤔 we are missing the `text` field
# must be because type of var docs: seq[ref DocItem]
# but above we just said this element is still "of TextDoc"
echo docs[0].text
# does not compile:
# Error: undeclared field: 'text'
It seems confused about whether elements of docs sequence have been truncated down to DocItem or still have their full type. I'm sure someone who knows Nim better can explain the semantics here?
And if it's going to squash them to fit the type of the sequence then it would be useful if Nim had proper sum types so you could widen the definition of the docs type.
We can find a description of two options for heterogenous sequences here: https://forum.nim-lang.org/t/4233#26367 The second is the one above ("boxed types") that doesn't work properly.
The first is using "object variants", which seems to be what Nim has instead of sum types. https://github.com/nim-lang/Nim/wiki/Common-Criticisms#sum-types-are-weird
The need to have a "discriminator" field to use in the case means we now have to make a custom % proc for our variant type, so that we can omit that superfluous field from the serialized output.
So you end up with this:
import json
type
DocKind = enum BaseDoc, TextDoc, TodoDoc
DocItem = object
tags: seq[string]
case kind: DocKind
of TextDoc: text: string
of TodoDoc: todo: string
else: discard
var docs: seq[DocItem]
docs.add DocItem(kind: TextDoc, text: "some doc", tags: @["help"])
docs.add DocItem(kind: TodoDoc, todo: "some todo")
proc `%`*(doc: DocItem): JsonNode =
result = case doc.kind
of TextDoc: %*
{"text": doc.text, "tags": doc.tags}
of TodoDoc: %*
{"todo": doc.todo, "tags": doc.tags}
else: %*
{"tags": doc.tags}
echo %docs
I spent a long time trying to use fieldPairs in the % proc and just filter out the kind field, but I could not get anything to work (it seems fieldPairs is some kind of fragile magic that only works in a for loop syntax position and gives an error in other places)
I'd love to know if there is a way to write this % proc that doesn't need to be updated each time you add a new kind to DocKind. It is fairly cumbersome as is.
I expect this will be detected at compile-time once we have the Z3 theorem prover:
type
DocKind = enum
text,
todo
Doc = object
case kind: DocKind
of text:
text: string
of todo:
todo: string
let doc = Doc(kind: text, text: "some doc")
echo doc.todo#compiles, throws exception
See https://nim-lang.org/docs/drnim.html This is similar to eliding array bounds check and probably a low-hanging fruit to experiment with a prover integrated in a compiler.
should it refuse to compile
I believe it should. I'm surprised that it does compile. I always access my variant fields in a case statement.
I think it should not :-). Even if generally we use a case statement to access to the fields according to the discriminator value, this is not the only way to proceed.
Here is an example:
type
ObjKind = enum objA, objB
Obj = object
case kind: ObjKind
of objA:
a: int
of objB:
b: string
proc isObjA(obj: Obj): bool =
obj.kind == objA
proc p(obj: Obj) =
if obj.isObja:
echo obj.a
Of course, the test done in the called proc may be much more complicated, so complicated that the compiler will not be able to detect the purpose of the proc.
There is another case. If you have done the check using a case statement somewhere, you can call a procedure with the object. Then you know what is the value of the discriminator in the called procedure and there is no need to check it again. Here is an example:
type
ObjKind = enum objA, objB
Obj = object
case kind: ObjKind
of objA:
a: int
of objB:
b: string
proc echoa(obj: Obj) =
# No need to check that the kind is objA, except if we want to avoid a fatal error at runtime.
echo obj.a
proc p(obj: Obj) =
case obj.kind
of objA:
obj.echoa()
of objB:
echo obj.b
This examples may seem silly. But maybe in some more complicated cases it could be useful. And, in any case, this is not the compiler job to decide what is good or bad programming.
If the compiler forced the use of a case statement before accessing the fields, this should become part of the language definition. But I’m not aware of language with this kind of rule. Not in Pascal, of course. And, as far as I remember, it doesn’t exist in Ada.
Nim being a strongly typed language must insure that during execution, no illegal access will be possible. That means that, if the field is not accessible, an error will be detected at runtime. That seems quite sufficient for me.
If the compiler forced the use of a case statement before accessing the fields, this should become part of the language definition. But I’m not aware of language with this kind of rule. Not in Pascal, of course. And, as far as I remember, it doesn’t exist in Ada.
Just as a data point, while this does indeed compile in Ada, including Ada/SPARK:
with Ada.Text_IO; use Ada.Text_IO;
procedure Test_Variant is
type ObjKind is (objA, objB);
type Obj (kind: ObjKind) is record
case kind is
when objA => a: Integer;
when objB => b: Character;
end case;
end record;
procedure EchoA(o: Obj) is
begin
put(o.a'Image);
end EchoA;
procedure p(o: Obj) is
begin
case o.kind is
when objA => EchoA(o);
when objB => Put(o.b);
end case;
end p;
o: Obj(objA);
begin
o.a := 4;
p(o);
end Test_Variant;
...as does this:
-- everything up to o's definition is the same
o2: Obj(objB);
begin
o.a := 4;
o2.b := 'c';
p(o);
EchoA(o2);
end Test_Variant;
...and it crashes on execution with a CONSTRAINT_ERROR, Ada/SPARK fails to find a proof, and reports an error exactly where you'd expect. I separated it into a library:
variantobj.adb:7:09: medium: discriminant check might fail (e.g. when O = (kind => objB, a => 0, b => 'NUL')) [possible explanation: subprogram at variantobj.ads:12 should mention O in a precondition][#0]
Well, and as @mratsim said this might be possible in the future once DrNim will get enough development :)