in the following D example, there is a function call mixin("arr[$-2] " ~ c ~ " arr[$-1]"); which make the string code as part of template fold!
void main()
{
import std.stdio, std.string, std.algorithm, std.conv;
// arr is real[] and sym is the current symbol
readln.split.fold!((arr, sym)
{
static foreach (c; "+-*/")
if (sym == [c])
// replace the last 2 elements with the binary op
return arr[0 .. $-2] ~
mixin("arr[$-2] " ~ c ~ " arr[$-1]");
// sym must be a number
return arr ~ sym.to!real;
})((real[]).init).writeln;
}
is there a equivalent mixin() in nim?
I did not know what RPN was... I asked to ChatGpt and it correctly identified that it is D programming language and what the code is doing, explaining RPN and each step of the code. Then I asked if it was able to do the same with Nim using its powerful macro system. It required three attempts, after each failed attempt I updtated ChatGpt session with error code output coming from Nim's playground... the fourth attempt appear to be working.
import macros, strutils, sequtils
# Define the macro to handle RPN parsing and evaluation
macro rpnCalculator(expr: static string): untyped =
# Split the input expression into tokens
let tokens = expr.splitWhitespace() # Split the input string into a sequence of tokens
var stack: seq[NimNode] = @[] # Stack to hold expressions
for token in tokens:
if token == "+":
if stack.len < 2: error("Invalid RPN expression: not enough operands for '+'")
let b = stack.pop()
let a = stack.pop()
stack.add(newCall(ident("+"), a, b))
elif token == "-":
if stack.len < 2: error("Invalid RPN expression: not enough operands for '-'")
let b = stack.pop()
let a = stack.pop()
stack.add(newCall(ident("-"), a, b))
elif token == "*":
if stack.len < 2: error("Invalid RPN expression: not enough operands for '*'")
let b = stack.pop()
let a = stack.pop()
stack.add(newCall(ident("*"), a, b))
elif token == "/":
if stack.len < 2: error("Invalid RPN expression: not enough operands for '/'")
let b = stack.pop()
let a = stack.pop()
stack.add(newCall(ident("/"), a, b))
else:
# For numbers, add them as literals to the stack
stack.add(newLit(token.parseFloat()))
# After processing all tokens, there should be exactly one result
if stack.len != 1:
error("Invalid RPN expression: too many or too few operands.")
result = stack[0]
# Main program
when isMainModule:
# Define an RPN expression to evaluate
let result = rpnCalculator("3 4 + 2 *") # RPN for (3 + 4) * 2
echo result # Output: 14
Pretty impressing, I've seen ChatGpt to improve a lot with Nim coding in the last year or so... but I was skeptical it could deal with macro.it works. but D is showing off with it's language syntax, where
readln. [code] . writeln finish the job..
I am just trying to improve my nim understanding, not making judgement...
here is what i have tried, partially working, but not completed yet
import macros
import strutils
const operators = ["+", "-", "*", "/"]
macro mixinS(s: static[string]): untyped = parseStmt(s)
let arr = @[1.0, 2.0, 3.0]
echo mixinS("arr[^1]" & operators[0] & "arr[^2]")
ChatGpt's produced code is quite verbose, the if section can be easily shortened
if token in ["+", "-", "*", "/"]:
if stack.len < 2: error("Invalid RPN expression: not enough operands for '+'")
let b = stack.pop()
let a = stack.pop()
stack.add(newCall(ident(token), a, b))
else:
# For numbers, add them as literals to the stack
stack.add(newLit(token.parseFloat()))
I find Nim very espressive and concise. As correctly pointed out, in many cases "code length" depends if a certain procedure is already available in standard libraries. In my opinion Nim offer very good libraries and a lot of syntactic sugar out of the box.I had a last try, it now works, but I do have some suggestions to nim syntax
import macros
import strutils
const operators = ["+", "-", "*", "/"]
macro mixinD(s: static[string]): untyped = parseStmt(s)
proc rpnOp(arr: var seq[float], sym: string) =
if sym in operators:
case sym
of "+": arr = arr[0..^3] & mixinD("arr[^2] + arr[^1]")
of "-": arr = arr[0..^3] & mixinD("arr[^2] - arr[^1]")
of "*": arr = arr[0..^3] & mixinD("arr[^2] * arr[^1]")
of "/": arr = arr[0..^3] & mixinD("arr[^2] / arr[^1]")
else: discard
else:
arr.add(sym.parseFloat)
var arr: seq[float] = @[]
let input = "1 2 3 + -"
for token in input.split():
rpnOp(arr, token)
echo arr
I know macros is the formal way for code generation, but some time good syntax also help stream line the thinking flow not to be diverged by a different way of thinking.
so i think introducing static foreach (op; "+-*/"): can be a good feature to have for nim template, togather with nim's macro mixinS(s: static[string]): untyped = parseStmt(s), will be better than D lang both reliability and conciseness
template rpnOp(arr: var seq[float], sym: string) =
static foreach (op; "+-*/"):
if (sym == [op])
arr = arr[0..^3] & mixinD("arr[^2]" & [op] & "arr[^1]")
else:
arr.add(sym.parseFloat)
Nothing about your code golfing is about "thinking flow not to be diverged", it's just you learning a new language.
Here is how I would do it:
import strutils
proc rpnOp(arr: var seq[float], sym: string) =
template reduce(opr) =
if astToStr(opr) == "`" & sym & "`":
let x = arr.pop()
arr[^1] = opr(x, arr[^1])
return
reduce(`+`)
reduce(`-`)
reduce(`*`)
reduce(`/`)
arr.add(sym.parseFloat)
var arr: seq[float] = @[]
let input = "1 2 3 + -"
for token in input.split():
rpnOp(arr, token)
echo arr
Something like "static foreach" can be implemented using macro.
Following code implements staticForeach macro using replace proc. replace is in https://github.com/demotomohiro/littlesugar/blob/main/src/littlesugar/replaceNimNodeTree.nim And uses Araq's code:
import std/[macros, strutils]
import littlesugar/replaceNimNodeTree
macro staticForeach(srcIdent: untyped; dstChars: static string; body: untyped): untyped =
srcIdent.expectKind nnkIdent
result = newStmtList()
for c in dstChars:
result.add replace(body, srcIdent, ident $c)
proc rpnOp(arr: var seq[float], sym: string) =
staticForeach(opr, "+-*/"):
if astToStr(opr) == sym:
let x = arr.pop()
arr[^1] = `opr`(x, arr[^1])
return
arr.add(sym.parseFloat)
var arr: seq[float] = @[]
let input = "1 2 3 + -"
for token in input.split():
rpnOp(arr, token)
echo arr
Is it the code logic of RPN properly implemented?
I didn't expect to work also with input like three numbers, followed by two operators, but it's a matter of notation.
Still I assume that
input = 3 4 - 5 * should return -5, but I get 5 instead
import strutils
proc rpnOp(arr: var seq[float], sym: string) =
template reduce(opr) =
if astToStr(opr) == "`" & sym & "`":
let x = arr.pop()
arr[^1] = opr(arr[^1], x)
return
reduce(`+`)
reduce(`-`)
reduce(`*`)
reduce(`/`)
arr.add(sym.parseFloat)
var arr: seq[float] = @[]
let input = "3 4 - 5 *" # (3 - 4) * 5
for token in input.split():
rpnOp(arr, token)
echo arr # @[-5.0]
Yes, it fixed. The maestro never disappoints!Thank you for the hint! I have got very close now,
import macros
import strutils
proc replace*(node, target, dest: NimNode): NimNode =
## Recursively replaces all `NimNode`subtrees in `node` that matches `target` with `dest`.
runnableExamples:
macro foo(target, dest, body: untyped): untyped =
replace(body, target, dest)
doAssert foo(min, max, min(3, 7)) == 7
if node == target:
result = dest.copyNimTree
else:
result = node.copyNimNode
for n in node:
result.add replace(n, target, dest)
macro staticForeach(srcIdent: untyped; dstChars: static string; body: untyped): untyped =
srcIdent.expectKind nnkIdent
result = newStmtList()
for c in dstChars:
result.add replace(body, srcIdent, ident $c)
macro mixins(s: static[string]): untyped = parseStmt(s)
#---------------------------------------------------------------------------------------------
proc rpnOp(arr: var seq[float], sym: string) =
staticForeach(c, "+-*/"):
if astToStr(c) == sym:
arr = arr[0..^3] & mixins("arr[^2] " & astToStr(c) & " arr[^1]")
return
arr.add(sym.parseFloat)
var arr: seq[float] = @[]
let input = "1 2 + 3 +"
for token in input.split():
rpnOp(arr, token)
the only thing left is to have some kind of operator like [c] which act as astToStr(c)
the only thing left is to have some kind of operator like [c] which act as astToStr(c)
Easy:
template `$.`(x: untyped): untyped = astToStr(x)
thanks @Araq, I learned so much about nim just by this single example :)
final solution here
macroutils.nim
import macros
proc replace*(node, target, dest: NimNode): NimNode =
## Recursively replaces all `NimNode`subtrees in `node` that matches `target` with `dest`.
runnableExamples:
macro foo(target, dest, body: untyped): untyped =
replace(body, target, dest)
doAssert foo(min, max, min(3, 7)) == 7
if node == target:
result = dest.copyNimTree
else:
result = node.copyNimNode
for n in node:
result.add replace(n, target, dest)
macro staticForeach*(srcIdent: untyped; dstChars: static string; body: untyped): untyped =
srcIdent.expectKind nnkIdent
result = newStmtList()
for c in dstChars:
result.add replace(body, srcIdent, ident $c)
macro mixins*(s: static[string]): untyped = parseStmt(s)
template `$.`*(x: untyped): untyped = astToStr(x)
rpn_cal.nim
import strutils
import macroutils
proc rpnOp(arr: var seq[float], sym: string) =
staticForeach(c, "+-*/"):
if $.c == sym:
arr = arr[0..^3] & mixins("arr[^2] " & $.c & " arr[^1]")
return
arr.add(sym.parseFloat)
var arr: seq[float] = @[]
let input = "1 2 + 3 +"
for token in input.split():
rpnOp(arr, token)
echo arr
original D code
// RPN Calculator
void main()
{
import std.stdio, std.string, std.algorithm, std.conv;
// arr is real[] and sym is the current symbol
readln.split.fold!((arr, sym)
{
static foreach (c; "+-*/")
if (sym == [c])
// replace the last 2 elements with the binary op
return arr[0 .. $-2] ~
mixin("arr[$-2] " ~ c ~ " arr[$-1]");
// sym must be a number
return arr ~ sym.to!real;
})((real[]).init).writeln;
}