Open wildthink opened 5 years ago
@wildthink the problem is that Expression doesn't support inout
arguments. The left hand side of =
is mutated by the assignment, but there's no way to do that because it's passed by value.
In an earlier version of the REPL I did implement =
as an operator, and instead of mutating the left hand side, the lhs value was used as a key into a global dictionary that the rhs value was actually assigned into.
I don't recall exactly why I changed it, but I think there was some kind of issue with Expression optimizing away the =
expression if the right-hand-side was constant (that problem may have since been solved).
Ah, is there no way to get the name of the symbol and/or have the symbol evaluate to itself in some fashion?
@wildthink correct. Symbol lookup is done separately prior to calling the operator implementation.
Using AnyExpression, you can do something like have every symbol evaluate to an instance of a custom class like BoxedValue which could then be used for assignment, but if you did that you’d have to overload all the standard math operators to accept BoxedValue operands instead of Double.
This is basically what I was trying to do. Any gotchas?
class Scope {
var variables:[String: Any] = [:]
func eval (_ e: String) -> Any? {
return self.eval(e) as Any
}
func eval<T>(_ e: String) -> T {
let parsedExpression = Expression.parse(e)
let expr = AnyExpression(parsedExpression, impureSymbols: { symbol in
switch symbol {
case .infix("="): return { args in
if let key = args[0] as? String {
self.variables[key] = args[1]
}
return args[1]
}
case .variable:
return { _ in
if let value = self.variables[symbol.name] {
return value
}
return symbol.name
}
default:
return nil
}
})
return try! expr.evaluate()
}
}
scope.eval("a = 5") // -> 5
scope.eval("a * 5") // -> 25
@wildthink that seems very similar to my original implementation.
What happens if you try to reassign a value to a
after you’ve assigned it the first time? I’m guessing you’ll get something like this:
scope.eval(“a = 5”) // 5
scope.eval(“a = 6”) // 6
scope.eval(“a”) // 5
That’s because once a
has been assigned a value it becomes that value and can no longer be reassigned, so the second expression here is basically 5 = 6
.
Rats! You got me. Have to think on that. Any ideas?
@wildthink you’d probably also get rather confusing errors if you try to use a variable before first assigning it. For
scope.eval(“5 * a”)
Instead of “symbol not found” the error would be a type mismatch because *
can’t accept a string operand.
I figured that and considered it somewhat acceptable. An undefined variable should be an error.
What do you think of having a "macro" option for functions and operators? Meaning, be definition, they get their arguments unevaluated and must explicitly evaluate any args as they choose/need.
@wildthink the BoxedValue solution I mentioned would work if you don’t mind having to reimplement the standard operators.
I can knock up an example for you if you like?
Thanks but don't put yourself out on my account. How big a job would that be to do them all? Is there a way to annotate a function definition with some meta data? Say, annotate an operator as being "numeric".
@wildthink it's no trouble. This is something that really ought to be supported properly, but I've not really had a need for it besides the REPL example. There are probably ways I could extend Expression to support it properly, but I'd have to think about how to do it without breaking API compatibility.
What do you think of having a "macro" option for functions and operators
Yeah, that's the kind of solution I had in mind. Adding new symbol types would be a significant breaking change though.
I'm looking through AnyExpression for the point where the arguments are evaluated to be passed to the function block to see how it's done.
umm, wondering if ParsedExpression.symbols was an ordered set then we reliably get the lhs of the = operator and nil it out.
@wildthink I've written a REPL implementation using a BoxedValue type instead. It works pretty well. I didn't have to reimplement any stdlib operators because I'm just unboxing the values and then calling the original implementations.
The only limitation is that this solution only works for the Expression stdlib (math and boolean operators), not for the extra stuff in AnyExpression. That means you can't use String values, for example, and Bool values print out as 1/0 not true/false. I expect you could get all that working with a bit of extra effort.
Here you go: https://github.com/nicklockwood/Expression/blob/boxed-value-repl/Examples/REPL/main.swift
Totally awesome. Thanks!
Looking at your REPL example, I was wondering why you manually subdivide the expression around the "=" sign rather than making the "=" a proper operator, executing the same logic in its definition.
Maybe there is something I'm not understanding as to how the framework works.
Thanks.