It looks like there is not a simple way to create a derived sequence without a certain element like how there is a way to do it with addition :+, +;. e.g. I expected
seq :- element
Only less direct approaches involving filtering.
Why is this?
It looks like there is not a simple way to create a derived sequence without a certain element like how there is a way to do it with addition :+, +;. e.g. I expected
seq :- element
Only less direct approaches involving filtering.
Why is this?
The most important reason is interop with postfix emoticons in the Lightbend Emoji library.
println("hello, world" :-)
The other reason is that +:
(which the editor wants to autocomplete to ) is by analogy to
::
for List
.
It has a corresponding extractor object. However, Seq
is considered not specific enough for âserious applicationsâ where you care about performance characteristics.
To strip the head, as when overtightening a screw or bolt, use xs.drop(1)
, or similarly for dropRight
.
All that is aside from a general bias against adding operator symbols without due cause.
However, I sympathize with the desire for symmetry and regularity.
println(`ÂŻ\_(ă)_/ÂŻ`)
Note that Set
does have -
:
Welcome to Scala 3.7.0 (21.0.7, Java OpenJDK 64-Bit Server VM).
scala> Set(1, 2, 3) - 2
val res0: Set[Int] = Set(1, 3)
with Seq
, itâs less clear that -
would make sense as an operator since the item in question might appear multiple times.
Should :-
remove the first aparece of the element? The last one? All elements? A random element? Should fail if there is no element?
I would also add that I rarely would need this.
Plus, the performance of this operation is probably O(N). So, if I would need to do this regularly such that filter
would be annoying then I may rather use a different data structure.
My immediate intuition was that it should remove the first occurrence of the element, and the more I interrogate this intuition the more easy to defend it seems.
:+
has the behavior of adding duplicate individuals, so we should expect itâs inverse -:
to also handle by individuals. And we are working with a Seq
not a Set
so this is already the precedent; just as you can add an item multiple times, you can remove it multiple times.No, âthe sequence of (1,2,3) without 8â is still (1,2,3). Idempotent operations are not failed operation, they are just trivial truths. Itâs inversely analogous to using filter
with a predicate which all elements satisfy, resulting in an unchanged collection; should we throw an exception because nothing was âtruly filteredâ? No, because the filter was applied successfully, and the -:
would be applied successfully, even if the results are unchanged.
scala> val xs = 1 to 10
val xs: scala.collection.immutable.Range.Inclusive = Range 1 to 10
scala> xs.patch(from = xs.indexOf(4), other = Nil, replaced = 1)
val res0: IndexedSeq[Int] = Vector(1, 2, 3, 5, 6, 7, 8, 9, 10)
scala> xs.span(_ < 4) match { case (a, b) => a ++ b.drop(1) }
val res1: IndexedSeq[Int] = Vector(1, 2, 3, 5, 6, 7, 8, 9, 10)
or mutably
scala> val xs = ArrayDeque.from(1 to 10)
val xs: scala.collection.mutable.ArrayDeque[Int] = ArrayDeque(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> xs.subtractOne(4)
val res3: scala.collection.mutable.ArrayDeque[Int] = ArrayDeque(1, 2, 3, 5, 6, 7, 8, 9, 10)
or
scala> val xs = ArrayDeque.from(1.to(10).toList)
val xs: scala.collection.mutable.ArrayDeque[Int] = ArrayDeque(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> val (a, b) = xs.span(_ != 4)
val a: scala.collection.mutable.ArrayDeque[Int] = ArrayDeque(1, 2, 3)
val b: scala.collection.mutable.ArrayDeque[Int] = ArrayDeque(4, 5, 6, 7, 8, 9, 10)
scala> a ++ b.drop(1)
val res5: scala.collection.mutable.ArrayDeque[Int] = ArrayDeque(1, 2, 3, 5, 6, 7, 8, 9, 10)
But there is a way, only not using a symbolic operator. Seq
has a diff
member:
In your case you can use seq.diff(Seq(element))
.
I think compared to +:
you might want to be aware even the implementation is not trivial, while prepending head is O(1), any element removal needs to be O(N) for a general unsorted sequence.
We? You may very well expect that, but I donât. :+
appends an element regardless of its value (and thus any notion of equivalence and thus duplication) to the lbs sequence. Contrast with Set
, as @SethTisue pointed out.
If youâre looking to debate some abstract point about inverse operations, look for how to deconstruct a sequence into an initial part and final element (likewise for the first part and tail). At which point you will move off Seq
and on to List
or the (presumably immutable) Deque
(from the Typelevel library).
If youâve got an actual use-case where you intend some pattern of interaction with your putative collection type, best to spell it out explicitly in your question. From what youâve described you want to be able to add and remove elements according to some notion of equivalence. Iâm not sure as to whether the order of addition of the elements is unimportant to you: in which case maybe a set or a bag is probably what you want.
If you feel the lack of some operator in your code base, you can always write an extension operator method that wraps, say the code that @som-snytt contributed.
While I also agree that the operation should not fail âper seâ. The point I am trying to make is that, for a Seq
, thinking of an operation that is the inverse of :+
& +:
doesnât make much sense at all.
:+
& +:
always returns a modified collection, but a hypothetical :-
& -:
may not. The already existing tail
and init
always return a modified collection, but donât care about the input, and actually fail on empty collections (which makes sense).
Sadly, there is not really a simple way to filter a single element.
The cleaner and most efficient solution is using a var
and break
for LinearSeqs
. Or, a combination of firstIndexOf
& patch
for IndexedSeqs
.
And, if you want to remove it from the back, then it is even harder.
However, as I said before, IME, it is rare to need this. Usually, this comes on very contrived code golfing exercises. And then, usually is better to just model the data differently.
This brings me to the point that @sageserpent-open was made.
If you have a real use case for this, then it is better to discuss that. But, I have a feeling that you just think the operation is missing and thus want to see it added, just because.
But, the stdlib does not and should not work like that. We should add features that make sense and are generally useful. Not just add stuff in the sake of âcompletenessâ.
Not only for practical reasons, but also because pursuing completeness in general is impossible, we can start thinking about a lot of variations to each operation, and at some point, the similarities start to get loose; as I pointed out at the beginning that such operations are not truly inverses as you think.
I agree with you that :-
and -:
have a natural interpretation, but what one wants much more often is to unbind the first or last element:
xs match
case ys :+ y => ???
So we already have the best inverse operation possible; itâs just that itâs pattern-matching so we can bind a new symbol to the last element slot, not name the symbol.
âRemove the first or last instance ofâ is an operation that I almost never use, mostly because Iâd never store things that way in the first place if I wanted to access them that way. Iâm normally going to use Map[Foo, Vector[Bar]]
or something.
In the very rare cases where I do need that pattern, Iâd probably either
var found = false
val ys = xs.filter(x => found || { x != y || { found = true; false } })
// Can inspect `found` to know if we found anything!
or
val ys = xs.indexOf(y) match
case i if i < 0 => xs // Can put more logic here to know we didn't find anything!
case i => xs.patch(i, Nil, 1)
because with both of these, unlike with xs :- y
, also allow me to tell if I found the thing I was looking for (which I quite often would want to know anyway).
(Note: these are the forward direction; one would use lastIndexOf
with the second method to get the last instance, and the first method is annoying to do but .reverse.filter(...).reverse
is one possibility.)
But, anyway, this is one of those cases which doesnât come up very often because when it does come up there was very often a better way to structure it earlier on. (This is not because the goal is to avoid this pattern. Itâs that youâd structure it a different way for its own merits but after having done that, this capability wouldnât be needed.)
(Note also that internal single-element deletion isnât very efficient for most immutable structures. If youâre carrying over algorithms that were assuming a mutable doubly-linked list or skip list, theyâre probably not well-adapted.)
I donât share the intuition that list :- x
should remove the last instance of x
from list
. There is not a clean metaphor for :+
, which is what Iâd expect here. The reason is that :+
always appends the element to the end of the sequence. :-
I would expect to remove the last element from the sequence, but you donât want a binary operator for that, and the method for it already exists as def init
.
I think if you really need this functionality, you can add extension methods to Seq
that are implemented with @Ichoran 's patch
idea. Thereâs nothing wrong with doing that, although it could get expensive to call these methods if the sequences are large, and especially if they are large and donât contain the element you want removed.