nicklockwood / Expression

A cross-platform Swift library for evaluating mathematical expressions at runtime
MIT License
824 stars 50 forks source link

Why isn't "=" an operator? #18

Open wildthink opened 5 years ago

wildthink commented 5 years ago

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.

nicklockwood commented 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).

wildthink commented 5 years ago

Ah, is there no way to get the name of the symbol and/or have the symbol evaluate to itself in some fashion?

nicklockwood commented 5 years ago

@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.

wildthink commented 5 years ago

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
nicklockwood commented 5 years ago

@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.

wildthink commented 5 years ago

Rats! You got me. Have to think on that. Any ideas?

nicklockwood commented 5 years ago

@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.

wildthink commented 5 years ago

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.

nicklockwood commented 5 years ago

@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?

wildthink commented 5 years ago

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".

nicklockwood commented 5 years ago

@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.

nicklockwood commented 5 years ago

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.

wildthink commented 5 years ago

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.

wildthink commented 5 years ago

umm, wondering if ParsedExpression.symbols was an ordered set then we reliably get the lhs of the = operator and nil it out.

nicklockwood commented 5 years ago

@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

wildthink commented 5 years ago

Totally awesome. Thanks!