Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Unanswered
KyleSanderson asked this question in General
Discussion options

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`)
You must be logged in to vote

Replies: 7 comments

Comment options

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.

You must be logged in to vote
0 replies
Comment options

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) })
....
You must be logged in to vote
0 replies
Comment options

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

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

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

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Converted from issue

This discussion was converted from issue #789 on September 18, 2025 12:03.

AltStyle によって変換されたページ (->オリジナル) /