Hi there.

I like the Python RAII pattern of "with" statements, alongside context managers.

eg, this is nice:

with open("test.txt") as f:
    print f.read()

Closest thing to that I could find was the "withFile" example over in the second Nim tut.

So I've tried to adapt that example a bit, and ended up with this:

template withAs(x: typed, y: untyped,
                body: untyped): typed =
  var x2 = x
  var y = x2.enter()
  try:
    body
  finally:
    x2.exit()

proc enter(f: File): File =
    echo "Entering context manager"
    return f

proc exit(f: File) =
    echo "Leaving context manager"
    f.close()

open("test.txt").withAs(f):
    echo f.readLine()

So when I run that, I get this output:

Entering context manager
HOW ARE YOU GENTLEMEN
Leaving context manager

So it looks like things are working.

Is "withAs" (and context managers) an existing well-known Nim pattern?

Would be nice to be able to eg, do "nimble install contextlib", and then have "withAs" as well as some other useful things.

There's actually a lot of places I'd really like to use "withAs" in, eg to make sure that resources are closed, but without needing to add a resource-specific "defer" line.

Comments?

2017-09-28 06:13:29
2017-09-28 10:27:37

Is "withAs" (and context managers) an existing well-known Nim pattern?

Yes, and you can take it further by providing template-only operations:

template withNumber(i: var int, body: untyped) =
  template add(b: int) {.used.} = i += b
  template sub(b: int) {.used.} = i -= b
  body

var n = 5
withNumber n:
  add(5)
  sub(1)

withNumber n, sub(1)
withNumber(n, add(1))
withNumber(n, (sub(2); sub(3);))

2017-09-28 11:36:04
@wizzardx and @Arrrrrrrrr , it's wonderful to have this feature. Thanks for the code snippets. 2017-09-28 12:33:38

Thanks for the replies

Is it possible to express the "withAs" template as a macro?

So that my example could look like this:

with open("test.txt") as f:
    echo f.readLine()

I think it should be possible, since eg nimpylib lets you use this syntax:

class Customer(object):
  """A customer of ABC Bank with a checking account. Customers have the
  following properties:
  Attributes:
      name: A string representing the customer's name.
      balance: A float tracking the current balance of the customer's account.
  """

eg, over here:

https://github.com/Yardanico/nimpylib/blob/master/examples/example2.nim

2017-09-29 10:21:21
with is a keyword, so you can't use that. You could use something like this maybe?
import macros

macro take(args,body:untyped):untyped =
  result = newstmtlist()
  result.add(newvarstmt(args[^1],args[1]))
  for st in body: result.add(st)

take 3 as f:
    echo f
(I haven't really used macros a lot, there's probably a better way to copy the nodes from body)
2017-09-29 10:52:53

Thanks!

So if I understand it correctly, this macro usage in your snippet:

take 3 as f:
    echo f

Gets expanded to this:

var f = 3
echo f

How can we change the same macro, so that it expands over to this instead?

var private_x = 3
var f = my_enter(private_x)
try:
    echo f
finally:
    my_exit(private_x)

Where:

1) private_x is a variable that isn't directly visible to the generated code; more like a private variable - something that only exists behind the scenes.

2) f is like in your example macro - it's the only variable that gets exported (and then used within the expanded statement block), so that the statement in the body can use it.

3) my_enter is a separate function that the user must supply, eg:

proc my_enter(x: int): int =
  echo "my_enter was called"
  return x

4) my_exit is a separate function that the user must also supply, eg:

proc my_exit(x: int) =
  echo "my_exit was called"

2017-09-30 07:56:03
It's very easy with "quote do":
import macros

proc my_enter(x: int): int =
  echo "my_enter was called"
  return x

proc my_exit(x: int) =
  echo "my_exit was called"

macro take(args, body: untyped): untyped =
  # Value of an argument
  let varValue = args[1]
  # Variable name
  let varName = args[^1]
  result = quote do:
    # Private variable would be "private", so it wouldn't be redefined
    var private = `varValue`
    # We need to have a block to make sure varName is not redefined
    block:
      var `varName` = my_enter(private)
      try:
        `body`
      finally:
        my_exit(private)

take 3 as f:
  echo f

take 6 as f:
  echo f
2017-09-30 10:23:02

As for now, template + getAst seems preferred over quote. Please note it provides you template hygiene, meaning you can choose whenever new symbols are generated or just injected into the scope (all symbols in quote-do are genSym-ed).

private variable can be defined inside the block. In fact, it probably should, as otherwise it doesn't fulfill the narrowest scope rule.

Also, grammar should be properly checked.

import macros
from strutils import `%`


proc my_enter(x: int): int =
  echo "my_enter was called"
  return x

proc my_exit(x: int) =
  echo "my_exit was called"

macro take(args, body: untyped): untyped =
  # Check the grammar
  if args.kind != nnkInfix or args.len != 3 or $args[0] != "as":
    error "'(value) as (name)' expected in take, found: '$1'" % [args.repr]
  
  # Value of an argument
  let varValue = args[1]
  
  # Variable name
  let varName = args[^1]
  
  # Prepare code
  template takeImpl(name, value, body) =
    block:
      var private {.genSym.} = value
      var name = my_enter(private)
      try:
        body
      finally:
        my_exit(private)
  
  # Generate AST
  getAst(takeImpl(varName, varValue, body))

take 3 as f:
  echo f

take 6 as f:
  echo f

# test if it provides a useful error message for grammar mistakes:
take 7 of f:    # error: '(value) as (name)' expected in take, found: '7 of f'
  echo f

# test if `private` is accessible:
take 7 as f:
  echo private  # error: undeclared identifier: 'private'

2017-09-30 17:29:55
Udiknedormin: you can change "take" to "with", since "with" and "without" were removed from Nim keyword list in devel
2017-09-30 18:25:27
<<<••12••>>>