expr icon indicating copy to clipboard operation
expr copied to clipboard

[]interface{} from map cannot be used as []string

Open KyleSanderson opened this issue 7 months ago • 6 comments

I've had to write this instead which is a bit wild... what am I doing wrong?

func ToStringSlice(input []interface{}) ([]string, error) {
	out := make([]string, len(input))
	for i, val := range input {
		str, ok := val.(string)
		if !ok {
			return nil, fmt.Errorf("element at index %d is not a string", i)
		}
		out[i] = str
	}
	return out, nil
}

func MustStringSlice(v interface{}) ([]string, error) {
	raw, ok := v.([]interface{})
	if !ok {
		return nil, fmt.Errorf("expected []interface{}, got %T", v)
	}
	return ToStringSlice(raw)
}
		"AddTags": func(hashes []interface{}, tags string) error {
			h, _ := exprutil.MustStringSlice(hashes)
			return c.Client.AddTagsCtx(ctx, h, tags)
		},

Query:

{"level":"trace","program":"test-program","query":"let tor = Imp.GetTorrents(nil); Imp.AddTags(map(filter(tor, .Name contains `The.Clams`), .Hash), `yams`)","error":"reflect: Call using []interface {} as type []string (1:37)\n | let tor = Imp.GetTorrents(nil); Imp.AddTags(map(filter(tor, .Name contains `The.Clams`), .Hash), `yams`)\n | ....................................^","time":1747295553,"message":"expr completed: <nil>"}

What I expected to work...

let tor = Imp.GetTorrents(nil);
     Imp.AddTags(map(filter(tor, .Name contains `The.Clams`), string(.Hash)), `yams`)

KyleSanderson avatar May 15 '25 08:05 KyleSanderson

Hi,

Yes, the map and filter builtin always return type is []any. This is by design. This makes those functions behave in an understandable way.

An example:

array | map(# % 2 == 0 ? "even" : 42)

What type of the returned expression should be?

Some versions ago, Expr would try to do some type checks and try to "inherit" type of predicate in map & filter. But this lead to a lot of confusion.

You're doing things right. Design your custom function to take []any. Cast to a proper type inside.

antonmedv avatar May 15 '25 08:05 antonmedv

Hi,

Yes, the map and filter builtin always return type is []any. This is by design. This makes those functions behave in an understandable way.

An example:

array | map(# % 2 == 0 ? "even" : 42)

What type of the returned expression should be?

Some versions ago, Expr would try to do some type checks and try to "inherit" type of predicate in map & filter. But this lead to a lot of confusion.

You're doing things right. Design your custom function to take []any. Cast to a proper type inside.

In that case, because it's untyped for some reason(?) which is illegal in a number of languages... it should be any. If both were clearly ints... int.

For what it's worth, this decision is workable with AI, but feels is absolutely ridiculous.

	wrap := func(fn any) func(...any) (any, error) {
		return func(params ...any) (any, error) {
			switch f := fn.(type) {
			case func([]string) error:
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				slice, err := toStringSlice(params[0])
				if err != nil {
					return nil, err
				}
				return nil, f(slice)
			case func([]string, bool) error:
				if len(params) != 2 {
					return nil, fmt.Errorf("expected 2 params, got %d", len(params))
				}
				slice, err := toStringSlice(params[0])
				if err != nil {
					return nil, err
				}
				b, _ := params[1].(bool)
				return nil, f(slice, b)
			case func([]string, string) error:
				if len(params) != 2 {
					return nil, fmt.Errorf("expected 2 params, got %d", len(params))
				}
				slice, err := toStringSlice(params[0])
				if err != nil {
					return nil, err
				}
				tag, _ := params[1].(string)
				return nil, f(slice, tag)
			case func([]string) (map[string]int64, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				slice, err := toStringSlice(params[0])
				if err != nil {
					return nil, err
				}
				return f(slice)
			case func([]string, int64) error:
				if len(params) != 2 {
					return nil, fmt.Errorf("expected 2 params, got %d", len(params))
				}
				slice, err := toStringSlice(params[0])
				if err != nil {
					return nil, err
				}
				limit, _ := params[1].(int64)
				return nil, f(slice, limit)
			case func([]string, float64, int64, int64) error:
				if len(params) != 4 {
					return nil, fmt.Errorf("expected 4 params, got %d", len(params))
				}
				slice, err := toStringSlice(params[0])
				if err != nil {
					return nil, err
				}
				ratio, _ := params[1].(float64)
				seed, _ := params[2].(int64)
				inact, _ := params[3].(int64)
				return nil, f(slice, ratio, seed, inact)
			case func([]interface{}, string) error:
				if len(params) != 2 {
					return nil, fmt.Errorf("expected 2 params, got %d", len(params))
				}
				slice, ok := params[0].([]interface{})
				if !ok {
					return nil, fmt.Errorf("param is not []interface{}")
				}
				tags, _ := params[1].(string)
				return nil, f(slice, tags)
			case func([]string, []string) error:
				if len(params) != 2 {
					return nil, fmt.Errorf("expected 2 params, got %d", len(params))
				}
				slice1, err := toStringSlice(params[0])
				if err != nil {
					return nil, err
				}
				slice2, err := toStringSlice(params[1])
				if err != nil {
					return nil, err
				}
				return nil, f(slice1, slice2)
			case func(qbittorrent.TorrentFilterOptions) ([]qbittorrent.Torrent, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				var filter qbittorrent.TorrentFilterOptions
				switch v := params[0].(type) {
				case nil:
					return f(filter)
				case map[string]interface{}:
					if err := mapToStruct(v, &filter); err != nil {
						return nil, err
					}
					return f(filter)
				case []interface{}:
					// treat []interface{} as empty filter (expr sometimes passes [] for nil)
					return f(filter)
				default:
					return nil, fmt.Errorf("expected map[string]interface{} or nil for filter options, got %T", params[0])
				}
			// Add more cases for other function signatures as needed
			// --- Additional qbittorrent types for wrap ---
			case func(string) ([]byte, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return f(hash)
			case func(string) (*qbittorrent.TorrentFiles, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return f(hash)
			case func(string) ([]string, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return f(hash)
			case func(string) ([]qbittorrent.PieceState, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return f(hash)
			case func(string) (qbittorrent.TorrentProperties, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return f(hash)
			case func(string) ([]qbittorrent.TorrentTracker, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return f(hash)
			case func(string) ([]qbittorrent.WebSeed, error):
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return f(hash)
			case func(string) error:
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				hash, _ := params[0].(string)
				return nil, f(hash)
			case func() (bool, error):
				return f()
			case func() ([]qbittorrent.Cookie, error):
				return f()
			case func() (qbittorrent.AppPreferences, error):
				return f()
			case func() (string, error):
				return f()
			case func() (qbittorrent.BuildInfo, error):
				return f()
			case func() (map[string]qbittorrent.Category, error):
				return f()
			case func() (int64, error):
				return f()
			case func() ([]qbittorrent.Log, error):
				return f()
			case func() ([]qbittorrent.PeerLog, error):
				return f()
			case func() ([]string, error):
				return f()
			case func() ([]qbittorrent.Torrent, error):
				return f()
			case func() (*qbittorrent.TransferInfo, error):
				return f()
			case func() error:
				return nil, f()
			case func([]qbittorrent.Cookie) error:
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				cookies, ok := params[0].([]qbittorrent.Cookie)
				if !ok {
					return nil, fmt.Errorf("param is not []qbittorrent.Cookie")
				}
				return nil, f(cookies)
			case func(map[string]interface{}) error:
				if len(params) != 1 {
					return nil, fmt.Errorf("expected 1 param, got %d", len(params))
				}
				prefs, ok := params[0].(map[string]interface{})
				if !ok {
					return nil, fmt.Errorf("param is not map[string]interface{}")
				}
				return nil, f(prefs)
			case func(string, string) error:
				if len(params) != 2 {
					return nil, fmt.Errorf("expected 2 params, got %d", len(params))
				}
				p0, _ := params[0].(string)
				p1, _ := params[1].(string)
				return nil, f(p0, p1)
			case func(string, string, string) error:
				if len(params) != 3 {
					return nil, fmt.Errorf("expected 3 params, got %d", len(params))
				}
				p0, _ := params[0].(string)
				p1, _ := params[1].(string)
				p2, _ := params[2].(string)
				return nil, f(p0, p1, p2)
			default:
				return nil, fmt.Errorf("unsupported function signature")
			}
		}
	}

	env["AddPeersForTorrents"] = wrap(func(hashes, peers []string) error { return c.Client.AddPeersForTorrentsCtx(ctx, hashes, peers) })
....

KyleSanderson avatar May 15 '25 18:05 KyleSanderson

This is very strange function. Please, explain what are your trying to do?

antonmedv avatar May 16 '25 07:05 antonmedv

This is very strange function. Please, explain what are your trying to do?

I have (and my hundreds of users) been using this as a hacked up language for writing small programs using embedded structs for the last 4 years. Sort, Action, and similar were layered on-top as seperate programs but that's not needed anymore with the latest developments.

The only thing that is actually missing is discrete functions within expr and this should be complete enough to move a lot of logic into the language. There's still a decent amount of odd bugs with the various representations of functions (map[string]any vs struct) but it's getting leagues better.

I know it's not the intention of the library, but I'm fairly confident this is close to being turing complete if development continues over the next couple years.

KyleSanderson avatar May 17 '25 00:05 KyleSanderson

I see. But why do you need functions inside expr?

antonmedv avatar May 17 '25 12:05 antonmedv

I see. But why do you need functions inside expr?

I want to ship a binary that doesn't change. Then I can ship configuration files as updates. Keeping this logic in the language allows for users to modify or remove pieces they don't want, and is far more reachable for an average user.

KyleSanderson avatar May 17 '25 22:05 KyleSanderson

I'm converting this to discussion, as I not complete anderstend what features/or bugs should be added.

Let's discuss this and figure out the missing parts.

antonmedv avatar Sep 18 '25 12:09 antonmedv