Skip to content

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

Description

@KyleSanderson

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`)

Activity

  1. antonmedv commented on May 15, 2025

    @antonmedv
    Member

    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.

  2. KyleSanderson commented on May 15, 2025

    @KyleSanderson
    Author

    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) })
    ....
    
  3. antonmedv commented on May 16, 2025

    @antonmedv
    Member

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

  4. KyleSanderson commented on May 17, 2025

    @KyleSanderson
    Author

    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.

  5. antonmedv commented on May 17, 2025

    @antonmedv
    Member

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

  6. KyleSanderson commented on May 17, 2025

    @KyleSanderson
    Author

    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.

  7. antonmedv commented on Sep 18, 2025

    @antonmedv
    Member

    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.

  8. locked and limited conversation to collaborators on Sep 18, 2025
  9. converted this issue into a discussion #837 on Sep 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions