Version 28 of lreplace

Updated 2014-02-08 06:45:08 by pooryorick

Summary

Replace elements in a list with new elements

Documentation

man page

Synopsis

lreplace list first last ?element element ...?

Description

lreplace returns a new list formed by replacing one or more elements of list with the element arguments. first gives the zero-based index in list of the first element to be replaced. If first is less than zero then it is interpreted as zero. first must correspond to the index of an existing element in list. Last gives the index in list of the last element to be replaced. If last is less than first then no elements are deleted; the new elements are simply inserted before first. first or last may be end (or any abbreviation of it), which denotes the last element of the list.

Each element is added to the list in place of those that are deleted. If element arguments are specified, then the elements between first and last are simply deleted.

Examples

% lreplace {a b c} 0 0 @
@ b c
% lreplace {a b c} 1 1 
a c
% lreplace {a b c} 1 1 hello there
a hello there c

In-place Modification of the List

If lreplace is handed an unshared list, it will grab the opportunity to modify the list in place, avoiding the processing overhead of making the copy, and the additional memory usage of the copy. First, an example of a list that is guaranteed to be shared:

set l [lreplace $l $j $j $newvalue]

Since the value of $l is bound to $l in the caller of lreplace, it is shared, and lreplace can not modify it in-place. What's needed is to unbind that value from $l after Tcl sees that it should be handed to replace, and before lreplace is actually invoked, i.e., sometime later on the command line:

set l [lreplace $l[set l {}] $j $j $newvalue]

Tcl substitutes the value $l, and then appends the empty string to that value, except that it doesn't, because appending the empty string is a no-op. In the meantime, thanks to set, $l is no longer bound to that value, so that value is unshared. Shortly thereafter, lreplace is called, and sees the unshared value.

In older versions of Tcl, before appending the empty string was a no-op, K was used:

set l [lreplace [K $l [set l {}]] $j $j $newvalue]

Set also Shuffle a list and lset

The following procedure accomplishes the same thing:

proc lipreplace {_list first last args} {
    upvar $_list list
    set list [lreplace $list[set list {}] $first $last {*}$args]
}
% set A [list 1 2 3]
1 2 3
% lipreplace A 1 end c d
1 c d

Sliding Values

If lreplace causes the size of the list to change, all remaining items in the list must be shifted up or down to accomodate the new size. Therefore, when iterating through a list of lists and deleting items, designing the process so that items toward the end of the list are replaced first can have a big speed benefit. For example, the list could be iterated from back to front:

for {set i [expr {[llength $list]-1}]} {$i >= 0} {incr i -1} {
        set list [lreplace $list[set list {}] $i $i]
}

Prepend, Yes, Append, No

AMG: While lreplace can be used to prepend elements to the beginning of a list, it cannot be used to append elements to the end of a list.

lreplace {0 1 2} 0 -1 x y z
→ x y z 0 1 2
lreplace {0 1 2} end -1 x y z
→ 0 1 x y z 2
lreplace {0 1 2} end+1 -1 x y z
→ error: list doesn't contain element end+1

The man page says "if last is less than first, then any specified elements will be inserted into the list at the point specified by first with no elements being deleted." Like lindex, it refers to string index for an explanation of the index notation and interpretation, so end is the last element, which is 2 in this case.

Clearly, the insertion takes place just before the first element, even when first is end-relative. This is inconsistent with linsert, which (despite the documentation) inserts just after the indexth element when index is end'-relative.

It is also inconsistent that lreplace accepts indexes before the beginning of the list but rejects indexes after the end of the list. linsert accepts out-of-bounds indexes in either direction.

By the way, to prepend and append individual elements or entire lists to a list, several combinations of concat, list, and {*} can be used. Or if in-place modification is desired, use lappend with or without {*}, or use lset one element at a time. For example, all of the following combinations return x y 0 1:

set elem1 0; set elem2 1
set list1 {x y}; set list2 {0 1}
Combine lists and elements using concat and list concat $list1 [list $elem1] [list $elem2]
Combine lists using concat concat $list1 $list2
Combine lists and elements using list and {*} list {*}$list1 $elem1 $elem2
Combine lists using list and {*} list {*}$list1 {*}$list2
Append elements to a list using lappend lappend list1 $elem1 $elem2
Append a list to a list using lappend and {*} lappend list1 {*}$list2
Append elements to a list using lset lset list1 end+1 $elem1; lset list1 end+1 $elem2

Bounds Checking

escargo 2005-09-15: Perhaps there is a better place for the following discussion, but this is where I saw a specific feature mentioned that I want to raise an issue about.

List commands bounds checking and associated behavior

The description above states (in part): "If first is less than zero then it refers to the first element of list."

I see two problems with this:

  • What should the behavior be when indexing outside the bounds of a list?
  • How does this behavior vary from other list operations?

Personally, I see allowing negative indexes (first or last) to be errors, and ones that are silent failures at that. (This is unlike Icon where at least some negative indexes have semantically valid meaning.) Perhaps list operations that do indexing should have a -strict option so that if indexes are outside the proper ranges, errors would be thrown.

On the second issue, lindex allows negative indexes (and indexes greater than the length of the list), but returns no values. The negative index is not coerced to zero. Thus the same index value (a negative one) refers to no element with lindex but the zeroth element with lreplace. Perhaps an item for the Tcl 9.0 WishList would be consistent behavior for list and string indexing semantics.

Misc

If the last argument of lreplace was made optional, one could essentially just alias ldelete to lreplace CmCc

AMG: I assume your suggestion is that omitting the last argument (plus all the element arguments to follow, of course) be taken to mean that the element at index first be removed from the output list. This seems reasonable to me.

My kneejerk complaint is that the name lreplace doesn't seem suitable for this usage, but this complaint already applies to the current case of omitting all element arguments, so no new naming problem is introduced.

See Also

linsert
lappend
list