Hi, this is my first post. I've been learning Nim for about 6 months now.
Consider this EXTREMELY SILLY and very dangerous code that casts and returns the correct value from two different sequences, based on an Enum value that acts as a switch:
type things = enum
word
number
var words = @["abc", "ABC", "AbC"]
var numbers = @[1,2,3]
proc get_right_thing [T](thing_type:things, index:int, t:typedesc[T]):ptr T =
var raw:pointer
case thing_type:
of word: raw = words[index].addr
of number: raw = numbers[index].addr
return cast[ptr T](raw)
echo get_right_thing(word, 2, string)[]
echo get_right_thing(number, 2, int)[]
This is of course a highly simplified version of what I'm trying to do. It works OK, but to make it safer I'd like to automatically choose the return type based on "thing_type", instead of letting myself in the future pick the wrong type and shoot myself in the proverbial foot.
I tried using "When T is int" and "When T is string" instead of a case block, and removing "t:typedesc" from the arguments, but I get "cannot instantiate T" errors, and I more or less understand why, but I can't figure out a solution.
Thanks!
Choosing a return type based on an int or enum value, make it seem like you want to decide at runtime what type is being returned. That won't work without an additional argument (e.g. your typedesc), in which case the case doesn't really serve a purpose, as (at least in theory) each case will match exactly one type, so you might as well only use typedesc without any int or enums.
However, if you are fine with deciding at compile time via an enum instead of a typedesc, you can do it as follows:
type things = enum
word
number
var words = @["abc", "ABC", "AbC"]
var numbers = @[1,2,3]
proc get_right_thing(thing_type: static things, index: int): auto =
var raw:pointer
when thing_type == word:
return cast[ptr string](words[index].addr)
elif thing_type == number:
return cast[ptr int](numbers[index].addr)
echo get_right_thing(word, 2)[]
echo get_right_thing(number, 2)[]
Note the static in front of things. This means it must receive a constant (i.e. compile time) value that specifies a "things".
For this simplified example, I'd always go with something like this though:
type things = enum
word
number
var words = @["abc", "ABC", "AbC"]
var numbers = @[1,2,3]
proc get_right_thing[T](index: int): auto =
var raw:pointer
when T is string:
return cast[ptr string](words[index].addr)
elif T is int:
return cast[ptr int](numbers[index].addr)
echo get_right_thing[string](2)[]
echo get_right_thing[int](2)[]
Here you'd get the "cannot instantiate T" error, if you do not specify the type via e.g. [string] as there is no argument that is also T from which to deduce the type.Thanks, those options are better than what I had!
But now that I think about it, I definitely need some way to decide what to return at runtime. I mean, I don't need it, but it would be nice to have, and less prone to mistakes since it would be automated. Is there a mechanism to achieve that in Nim? Maybe some void pointer craziness?
Long story short, it's used in a hierarchical node system, and each node can "point" to a valid thing subtype. Nodes have a "thing_type" int, and a "thing_index" int which represents the index in a sequence. Each Thing subtype has its own sequence so that they're laid out contiguously in memory. I have a way to "register" new Thing subtypes, each valid type gets a unique int id. Everything is an object in a sequence, not a ref object.
Retrieving the Node from the Thing is easy. Since all nodes are of type Node and are stored in a single sequence, each Thing can store the index to it. But retrieving the Thing from the Node is trickier, since I need pick the appropriate Thing sequence and cast the result.
That's what I'd like to automate. I haven't dabbled in Macros yet, still scared of it, but maybe it's a way out of that pickle?
I haven't dabbled in Macros yet, still scared of it, but maybe it's a way out of that pickle?
Macros don't help you with anything that you cannot / don't know how to write "by hand". However, often a solution doesn't feel like one, because it's too much boilerplate. Macros allow you to turn those into a usable solution.
Think about your problem this way: Whenever you need to "get" something from some object, you need some way to specify the return type of the associated function. This must be determined at compile time. Whether this happens by writing the type manually or by receiving some other input of the same type matters not. But there's no way around it. Sure, using void pointers you can return arbitrary data that, if cast to the correct type afterwards, allows you to return multiple types. But the required cast after is exactly the same thing (in terms of telling the compiler what type something is) than just having a proc that takes an additional generic / typedesc argument.
If you really want pure runtime logic without specifying things at CT, one option is to wrap the returned types all in a single unified variant object (like the JsonNode of the stdlib for example).
type
NodeValueKinds = enum
nkString, nkInt
NodeValue = object
case kind: NodeValueKinds # could use your thing enum of course
of nkString: s: string
of nkInt: i: int
This way the NodeValue object can have a kind that is determined at runtime and you can hide away whatever logic behind it. You trade CT safety and performance by convenience and more overhead.Thank you so much for the insights!
I think most of my difficulties come from how hard it is to shed old habits shaped by dynamic languages. I do love Nim so far, though, despite it being way more complex than I initially assumed.
Cheers!
I definitely need some way to decide what to return at runtime
Nim resolves statically and this is a good thing. If you want it dynamic, you should go for Objective C, Ruby (Crystal) or Javascript. Nothing wrong with these languages, they have a different purpose, they work well in and for very heterogeneous environments. Speed is not a concern (the compilers now do optimize very well) , but runtime safety is.