How can I get safe reference to existing object in Nim?
Asked Answered
R

2

5

I wrote a Nim procedure taking two objects as var parameters. They each are an object with an int field level. Before doing the real work of the procedure, I want to put the parameters in order by which has the larger level, so I do this:

proc subroutine(param1: var MyObject, param2: var MyObject) =
  var large: ptr MyObject = param1.addr
  var small: ptr MyObject = param2.addr

  # If in wrong order by level, swap large and small
  if large.level < small.level:
    large = param2.addr
    small = param1.addr

  # The rest of the proc references, only variables large and small, e.g.,
  large.level += 1
  small.level += 2

This seems to work for my application, but I notice that in the Nim documentation, the ptr type is called "unsafe", and it is suggested to be used only for low-level operations. There's a "safe" reference type ref, and it is suggested to use ref unless you really want to do manual memory management.

I don't want to do manual memory management, and I want the Nim garbage collector to handle the memory for these parameters for me, but I don't see a way to get safe ref's to the two parameters.

I really want to be able to write the algorithm (which is significantly more complex than the simple code I showed) in terms of the variables large and small, rather than param1 and param2. Otherwise, if I can only reference parameters param1 and param2, without making these aliases for them, I'd have to copy and paste the same algorithm twice to separately handle the cases param1.level < param2.level and param1.level >= param2.level.

Is there a more idiomatic Nim way to do something like this, without using a ptr type?

Rickart answered 8/10, 2020 at 1:23 Comment(0)
C
5

There is no way you can turn a safe objecto into unsafe and viceversa, except by performing a copy of the object.

Normal variables are stored on the stack and thus will be destroyed when their function scope exists, but a reference can be stored into a global variable and accessed later. If this was possible, to make it safe, the compiler/language would need to know some way of extracting the variable from the stack so that it's still valid after its scope exists, or perform a copy manually behind your back, or some other magical thing.

That's also the reason why taking the address of a variable is unsafe. You can't guarantee its lifetime, because you could store that address somewhere else and try to use it later. However, in terms of memory guarantees, those variables should be kept alive at least for the time of your proc call, so it should be safe to use those address aliases within that proc without worrying.

That said, you could rewrite your code to use an intermediate proxy proc that performs the check and thus passes the correct variables in each slot. The intermediate proc guarantees that one of the variables will always be large, and you can avoid using unsafe references:

type
  MyObject = object
    level: int

proc subroutine_internal(large: var MyObject, small: var MyObject) = 
  assert large.level >= small.level
  large.level += 1
  small.level += 2

proc subroutine(param1: var MyObject, param2: var MyObject) =
  if param1.level < param2.level:
    subroutine_internal(param2, param1)
  else:
    subroutine_internal(param1, param2)

proc main() =
  var
    a = MyObject(level: 3)
    b = MyObject(level: 40)

  subroutine(a, b)
  echo a, b

main()
Cowpox answered 8/10, 2020 at 7:27 Comment(2)
Thanks! Would you say it's more idiomatic Nim (or safer/more recommended) to use your solution, instead of using ptr? Or perhaps it's more idiomatic Nim not to use var parameters so freely, and instead use a more functional approach, returning new objects that represent modified versions of the parameters?Rickart
@DaveDoty wouldn't really know since I don't follow any Nim trends or look at what others do. I'd prefer the non ptr version in terms of safety, you might return in a few months, look at those pointers and do something you weren't meant to do when you first wrote the code. Nim can be pretty low level and imperative, so I wouldn't coerce stuff into functional style unless you really prefer it or it gives you some advantage.Cowpox
S
2
type
  MyObject = object
    level: int

proc subroutine(param1, param2: var MyObject) =
  if param1.level > param2.level:
    swap(param1, param2)
  echo param1.level
  echo param2.level

var
  p1 = MyObject(level: 7)
  p2 = MyObject(level: 3)

subroutine(p1, p2)
p2.level = 13
subroutine(p1, p2)

Nim has the special swap() proc for that case. Var parameters are passed internally as pointers, so in parameter passing no copy is involved, and swap should do the copy as optimal as possible.

Of course, there can be cases where using ref objects instead of value objects can have advantages. Nim's references are managed pointers. You can find more information in the tutorials, and my one is located at http://ssalewski.de/nimprogramming.html#_value_objects_and_references.

Shimkus answered 15/10, 2020 at 15:58 Comment(3)
This is very cool, I keep learning from you, @Salewski, but this code modifies p1 and p2 so the second output is (3,13) when it's expected to be (7,13)Wicket
Well the code does what I expected and what I thought was intended -- some sort of sorting the parameters by level field. The questions is if we really can trust swap() to only swapping pointers internally but not doing expensive copy of data content. If performance is really critical and we do not trust swap() then we would have to test or look at generated assembly (swap is an intern magic compiler proc). But the method of Mr Hankiewicz is also fine.Shimkus
At the end I don't like my own answer any more, as I do not fully understand what is really intended. And as both parameters are var parameters, swap() would indeed copy the content, so it may be not what was asked for, sorry. But why not just a call like "if a.level < b.level: p(a, b) else: p(b, a)"? That would be similar to the Hankiewicz answer.Shimkus

© 2022 - 2024 — McMap. All rights reserved.