-
-
Notifications
You must be signed in to change notification settings - Fork 463
[]interface{} from map cannot be used as []string #837
-
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`)
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 7 comments
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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) })
....
Beta Was this translation helpful? Give feedback.
All reactions
-
This is very strange function. Please, explain what are your trying to do?
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
I see. But why do you need functions inside expr?
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.