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?
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);))
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
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)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:
proc my_enter(x: int): int =
echo "my_enter was called"
return x
proc my_exit(x: int) =
echo "my_exit was called"
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
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'
Thanks for the replies!
Would other people find it useful if I made and uploaded a "pythonwith" nim package for this?
iterator myfunc(name: string):
var myfile = open(name)
yield myfile
myfile.close()
var myit = myfunc(filename)
with context(myit) as f:
...
or maybe
withcontext myit as f:
...
I haven't tried this, but maybe something like:
macro withcontext(args, body: untyped): untyped =
var varValue = args[1]
var varName = args[^1]
...
template takeImpl(thisit, name, body) =
block:
var private {.genSym.} = thisit
var name = private()
try:
body
finally:
private()
assert private.finished()
# Generate AST
getAst(takeImpl(varValue, varName, body))
Why not do something like:
template withFile(f: string, mode: string, statements: untyped) =
var fileMode: FileMode
case mode
of "r": fileMode = FileMode.fmRead
of "w": fileMode = FileMode.fmWrite
of "a+": fileMode = FileMode.fmReadWrite
of "r+": fileMode = FileMode.fmReadWriteExisting
var file {.inject.} = open(f, fileMode)
defer: file.close()
statements
usage example:
withFile("/path/to/file", "r"):
while not endOfFile(file):
echo(file.readLine())
@Arrrrrrrrr: I haven't seen a package like that, have googled this subject a few times before. Point me at it?
@Benjaminel: I'm trying to emulate the Python "with" style, which is more general-purpose than just files. I think that "using" in C# is similar, to take any given resource for a block, and then auto-close it for you at the end of that block.
@cdunn2001: I don't have any idea how to do that! For now I'm mainly interested in a python-like "with" statement; the other stuff is too advanced for me :-/, eg to give an example of how you'd expand the one to the other. I think that's more on the level of how Dom managed to hack in async and await with just macros.
@All:
Thanks for the replies so far! So, another question :-)
Can you update the macro so that it can work without an "as X" clause?
eg, something like this:
echo "I'm anywhere on the filesystem"
with myTempDirChange("/tmp/x"):
echo "I am now under /tmp"
echo "I'm back to where I was before"
Which then would expand to something along these lines:
echo "I'm anywhere on the filesystem"
var private_x = myTempDirChange("/tmp/x")
try:
echo "I am now under /tmp"
finally:
my_exit(private_x)
echo "I'm back to where I was before"
Where myTempDirChange records the current dir, stores that in an object, then returns that object. And my_exit returns to that directory contained in that temporary object. I'm a bit hazy on exact details - whether there is still an intermediate my_enter function needed or not.
Well, basically how idiomatic Python would handle something like this behind the scenes:
print("I'm anywhere on the filesystem")
with my_temp_dir_change("/tmp"):
print("I'm now under /tmp")
print("I'm back to the directory I was at before")
This is more of a toy example; other things might be locks, or other cases where you might normally use a "defer" statement to do some kind of tidy-up.
Another random example:
echo "sdl2 not yet initialized"
with my_init_sdl2():
echo "SDL2 is now initialized"
# Over here do your regular SDL2 logic, but don't need to worry about
# calling sdl2.quit yourself.
echo "sdl2 was shut down"
Would like to be able to have a "with" macro that supports both forms, based on whether there is an "as" clause.
Thanks!
Since you don't want to dive into macros and you don't need anything super-generic, I think you're better off with @Benjaminel's idea:
template withcd(newdir: string, statements: untyped) =
let olddir = os.getCurrentDir()
os.setCurrentDir(newdir)
defer: os.setCurrentDir(olddir)
statements
import os
proc hi() =
echo "in:", os.getCurrentDir()
hi()
withcd("bar"):
echo "It works!"
hi()
echo "Finish it."
hi()
in:/foo
It works!
in:/foo/bar
Finish it.
in:/foo