lent, move, sink

Idk why I'm learning about this

2024-11-18

#lent #move #nim #sink

After playing with Nim a whole lot yesterday, I finally managed to piece together some of this stuff.

lent

Essentially, we are telling the compiler: “Hey, this variable is supposed to be borrowed. Please make sure the references to it are valid”. Which means that even if what we’re referencing is mutable, we can’t do things willy-nilly:

{.experimental: "views".} # this needs to be enabled

type Dog = object
  name: string

# doesn't work
proc doStuffWithName(d: var Dog) =
  let name: lent string = d.name
  d.name = "new name"
  echo name

# is fine
proc doStuffWithNameOk(d: var Dog) =
  let name: lent string = d.name
  echo name
  d.name = "new name"

From the docs regarding “view types”:

A local variable of a view type borrows from the locations and it is statically enforced that the view does not outlive the location it was borrowed from.

For the duration of the borrow operation, no mutations to the borrowed locations may be performed except via the view that borrowed from the location. The borrowed location is said to be sealed during the borrow.

We can’t do stuff with name in the first example. We’re trying to mutate the original reference while its being borrowed, which the compiler rejects as an invalid program.

Similarly, we borrow name in the second proc - however, the difference is, we’re not doing anything with name after printing it: there are no more references to name within that scope, and the compiler seems to be able to tell, so it allows us to proceed with mutation.

sink

From the docs on “Sink parameters”:

If it cannot be proven to be the last usage of the location, a copy is done instead

proc main() =
  proc eat(a: var String, b: sink String) =
    a.val &= b.val
    b.val = ""

  var fat = String(val: "Fat")
  var thin = String(val: "Thin")
  fat.eat(thin)
  echo("Thin is now: ", thin)
  echo("Fat is now: ", fat)

main()

Prints this:

Thin is now: (val: "Thin")
Fat is now: (val: "FatThin")

Which makes sense? Since we’re using thin afterwards at echo("Thin is now: ", thin).

We can prove this:

type
  MyType = object
    data: int

proc `=copy`(dest: var MyType, src: MyType) =
  echo "COPIED"
  dest.data = src.data

proc `=destroy`(obj: var MyType) =
  echo "DESTROYING: " & $obj.data

proc own(obj: sink MyType) =
  echo "SINKING: " & $obj.data

var a = MyType(data: 42)
own(a)
echo a.data  # copy called

# COPIED
# SINKING: 42
# DESTROYING: 42
# 42
# DESTROYING: 42

I assume that “COPIED” gets printed first because the compiler moves some stuff around.

If we comment out echo a.data, this is what gets printed:

SINKING: 42
DESTROYING: 42
DESTROYING: 0

We see that “COPIED” is nowhere to be found. What I don’t get is why “DESTROYED” gets called twice. Oh well… That’s a mystery for another day.

move

If we want to ensure that a value gets consumed, we can do this:

foo.bar(move baz)

Then the proc takes ownership of the variable and the variable gets reset to its default value.

Conclusion

All of this is still extremely confusing and I’ll probably never use it, but I suppose its nice to know.