Welcome to the Inedo Forums! Check out the Forums Guide for help getting started.

If you are experiencing any issues with the forum software, please visit the Contact Form on our website and let us know!

Suggestion: allow for setting list or map elements by dynamic index or key (@ListSet, %MapSet)



  • It does not appear to be possible to set the value of a list or map element, where the index or key is stored in a variable.

    The formal grammar suggests...

    <assign_variable_statement> ::=
       set [local | global] ( /variable_expression/ | /indexed_expression/ ) = /literal_expression/;
    

    variable_expression:
    A variable type identifier ($, @, or %) immediately followed by one of:

    • simple name - follows same rules as any_name
    • explicit name - a left curly brace ({), followed by of characters with the same rules as any_name but that also allow spaces, followed by a right curly brace (})

    indexed_expression:
    A variable_expression for a vector (@) or map (%) type, immediately followed one of:

    • left bracket ([), scalar_expression, then right bracket (])
    • dot (.) then scalar_expression

    ...which implies that both...

    set %Map.$k = something;
    set @Vec[$i] = something;
    

    ...should be possible, but these throw various errors at the execution engine level, e.g.:

    Unhandled exception: System.InvalidCastException: Unable to cast object of type &#39;System.ArrayEnumerator&#39; to type &#39;System.Collections.Generic.IEnumerator`1[Inedo.ExecutionEngine.RuntimeValue]&#39;.
      at Inedo.ExecutionEngine.RuntimeListValue.GetEnumerator()
      at Inedo.ExecutionEngine.Mapping.CoreScriptPropertyMapper.CoerceValue(RuntimeValue value, PropertyInfo property, Type type)
      at Inedo.Otter.Service.PlanExecuter.ExecutionVariableEvaluationContext.GetVariableFunctionInternal(RuntimeVariableName functionName, IList`1 arguments)
      at Inedo.Otter.Service.PlanExecuter.ExecutionVariableEvaluationContext.TryEvaluateFunctionAsync(RuntimeVariableName functionName, IList`1 arguments)
      at Inedo.ExecutionEngine.Variables.FunctionTextValue.EvaluateAsync(IVariableEvaluationContext context)
      at Inedo.ExecutionEngine.Variables.ProcessedString.EvaluateValueAsync(IVariableEvaluationContext context)
      at Inedo.ExecutionEngine.Executer.ExecuterThread.EvaluateAsync(EqualityPredicate equalityPredicate)
      at Inedo.ExecutionEngine.Executer.ExecuterThread.ExecuteAsync(PredicateStatement predicateStatement)
      at Inedo.ExecutionEngine.Executer.ExecuterThread.ExecuteNextAsync()
    

    In the absence of syntax support, the sanest workaround is probably a pair of variable functions -- e.g. @ListSet(@Vec, $i, value) and %MapSet(%Map, $k, value), which could perform this operation.

    My current workaround is to...

    set %Map = %MapAdd(%MapRemove(%Map, $key), $key, value);
    
    set @copy = @();
    foreach $j in @Range(0, $ListCount(@Vec)) {
        if ($i == $j) { set @copy = @ListInsert(@copy, value); }
        else { set @copy = @ListInsert(@copy, $($ListItem(@Vec, $j))); }
    }
    set @Vec = @copy;
    

    ...the latter of which is so tortuous I've probably got it wrong just typing it in here (and does not handle anything other than lists of scalars).

    Looking at the existing similar %MapAdd and @ListInsert functions, I expect the meat of these is probably fairly straightforward:

    ListSetVariableFunction

            protected override IEnumerable EvaluateVector(IVariableFunctionContext context)
            {
                var list = this.List.ToList();
                var index = this.Index;
    
                // bounds checking
                if (index >= list.Count)
                {
                    // allow for growing the list to fit new index
                    list.AddRange(Enumerable.Range(0, 1+index-list.Count).Select(_ => string.Empty));
                }
                else if (index < 0)
                {
                    // allow for negative indexing from end of array (but not growth)
                    if (-index >= list.Count) throw new ArgumentOutOfRangeException(nameof(this.Index));
                    index = list.Count + (index % list.Count);
                }
    
                list[index] = this.Value;
                return list;
            }
    

    MapSetVariableFunction

            public override RuntimeValue Evaluate(IVariableFunctionContext context)
            {
                if (String.IsNullOrEmpty(this.Key)) throw new ArgumentNullException(nameof(this.Key));
    
                var map = new Dictionary<string, RuntimeValue>(this.Map);
                map[this.Key] = this.Value;
                return new RuntimeValue(map);
            }
    

    (Reposted from GitHub on request)


  • inedo-engineer

    @jimbobmcgee thanks for reposting this here

    This is a long-standing behavior of Otter/OtterScript and it's most likely not a trivial fix and would involve updating the parser/execution engine (after remembering how it all works) - so not something we'll do in a maintenance release for a community/free user, as I'm sure you'll understand

    However now that it's here, I will link it to our internal roadmap planning and consdieration for Otter 2025.



  • @dean-houston I'm sure that fixing the syntax would not be straightforward, but introducing @ListSet and %MapSet variable functions in lieu is probably a good enough workaround for nearly all use cases where someone would want to do this.


  • inedo-engineer

    @jimbobmcgee that's a nice idea; we'd definitely be open to a pull request on those FYI

    Based on other list/map functions I think it'd be relatively straight-forward and an easy pattern to follow:
    https://github.com/Inedo/inedox-inedocore/blob/master/InedoCore/InedoExtension/VariableFunctions/Lists/ListRemoveVariableFunction.cs

    Just not something we can focus on now though




  • inedo-engineer

    @jimbobmcgee fantastic, we'll review/merge soon! thanks much :)


Log in to reply
 

Inedo Website HomeSupport HomeCode of ConductForums GuideDocumentation