expr icon indicating copy to clipboard operation
expr copied to clipboard

Functions defined on the struct are bidden to the Env they were compiled with

Open vmihailenco opened this issue 1 year ago • 6 comments

For example, the following program can't be reused with different environments, because functions are bidden to the Env they were compiled with:

package main

import (
	"fmt"

	"github.com/expr-lang/expr"
)

type Env struct {
	Value string
}

func (e *Env) Get(args ...any) (any, error) {
	return e.Value, nil
}

func main() {
	code := `get()`

	env := Env{Value: "<empty>"}
	program, err := expr.Compile(code,
		expr.Function("get", env.Get, new(func() any)),
		expr.Env(env))
	if err != nil {
		panic(err)
	}

	output, err := expr.Run(program, Env{Value: "hello"})
	if err != nil {
		panic(err)
	}
	fmt.Println(output)

	output, err = expr.Run(program, Env{Value: "world"})
	if err != nil {
		panic(err)
	}
	fmt.Println(output)
}

The program output:

<empty>
<empty>

Expected:

hello
world

I can patch the code to pass $env as the first arg of the function, but then users get weird error messages if compilation fails.

vmihailenco avatar Aug 09 '24 09:08 vmihailenco

This is intended behaviour. You passed env.Get to Function:

expr.Function("get", env.Get, new(func() any)),

In Golang this makes reciver argument bidden to env. You can simplify your code and do not use expr.Function and use methods on env directly.

Please, try this approach and see if it works.

antonmedv avatar Aug 09 '24 13:08 antonmedv

You can simplify your code and do not use expr.Function and use methods on env directly.

But it will be slower and I can't use type hints to get helpful error message, right? It is not the trade-off I am willing to make...

vmihailenco avatar Aug 09 '24 14:08 vmihailenco

It will be just a bit slower. Types will be inherited.

antonmedv avatar Aug 10 '24 02:08 antonmedv

If I understood correctly, what you trying to achieve you can try this approach: create a function which will be bidden to and env outside of expr. Use expr.Function to call it.

This way you can achieve fastest calling speed and preserve bindings.

I will try to write some code when I can reach my computer

antonmedv avatar Aug 10 '24 02:08 antonmedv

The program I've shared gives a good example of what I am trying to achieve:

  • Compile a program once and then run it with different struct envs.
  • Functions must have access the current/running env, because I don't have vars and use getter functions instead. It is much more flexible this way.

Currently I am patching functions to pass $env as the first arg. It would be nice if expr-lang could detect this and pass it automatically, i.e. support functions with this signature:

var funcGetAttr = expr.Function(
	"getAttr",
	func (env expr.Env, args ...any) (any, error) {
		fmt.Println(env.(*myenv), args[0].(string))
        },
	new(func(string) string),
)

vmihailenco avatar Aug 10 '24 06:08 vmihailenco

This is a nice idea. Expr already has patcher for context. We can have a patcher for env as well.

antonmedv avatar Aug 10 '24 07:08 antonmedv