This works:

MapAt[h, {a, {b, c}, {d, e}, {f}}, 2 ;; 3]

(* {a, h[{b, c}], h[{d, e}], {f}} *)

but this doesn't:

FlattenAt[h, {a, {b, c}, {d, e}, {f}}, 2 ;; 3]

FlattenAt::argt: FlattenAt called with 3 arguments; 1 or 2 arguments are expected. >>

What is a performant way to make a custom flattenAt function that works on the same position arguments as MapAt?

  • 1
    $\begingroup$ What about something like MapAt[Apply@Sequence, {a, {b, c}, {d, e}, f}, 2 ;; 3]? $\endgroup$ – Leonid Shifrin Jan 7 '16 at 16:52
  • $\begingroup$ @Leonid Shifrin, I really like the solution from your comment; you should post it as an answer. The custom FlattenAt function from my answer seems to be a bit faster though, especially for a long list of positions. $\endgroup$ – Sascha Jan 7 '16 at 19:43
  • $\begingroup$ @Sascha Ok, why not. Posted. $\endgroup$ – Leonid Shifrin Jan 7 '16 at 19:49
  • 2
    $\begingroup$ @Leonid As I'm sure you know Apply will not work inside a held expression so this is not equivalent to FlattenAt. $\endgroup$ – Mr.Wizard Jan 29 '16 at 1:03
  • 1
    $\begingroup$ @Mr.Wizard I actually pretty much never use FlattenAt, so wasn't aware that it works inside held expressions - which is of course natural to expect once we think about it (in fact didn't even think in that direction, since typically my uses of held code and Flatten don't overlap). But that's a good point. I will make a note in my answer. Thanks for spotting this. $\endgroup$ – Leonid Shifrin Jan 29 '16 at 1:16
FlattenSpan[li_, sp_Span] :=
 FlattenAt[li, List /@ Range @@ sp]

FlattenSpan[{{a}, {b, c}, {d, e}, {f}}, 2 ;; 3]

enter image description here

| improve this answer | |
  • 1
    $\begingroup$ +1. If the list of positions is long, Transpose[{Range@@sp}] will be vastly more efficient than List /@ Range @@ sp. $\endgroup$ – Leonid Shifrin Jan 7 '16 at 18:11
  • 4
    $\begingroup$ You could use List /@ Range[Length@li][[sp]] to accommodate All and negative positions in the span. $\endgroup$ – Simon Woods Jan 7 '16 at 20:55
  • 2
    $\begingroup$ @Leonid and Partition[Range @@ sp, 1] will be more efficient still. ;-) $\endgroup$ – Mr.Wizard Jan 29 '16 at 1:02
  • 1
    $\begingroup$ @Leonid Sincerely glad if I can help, but I was mostly just having some fun. Frankly since I have seen the performance of various methods trade places between versions I have mostly stopped this kind of "micro benchmarking" as it is ephemeral knowledge. $\endgroup$ – Mr.Wizard Jan 29 '16 at 1:08
  • 1
    $\begingroup$ @Mr.Wizard I stopped doing that too, long ago, and for the same reason. Besides, in any large program (or even not so large), typically the bottlenecks are elsewhere. $\endgroup$ – Leonid Shifrin Jan 29 '16 at 1:15

Here is a version of FlattenAt - like function based on MapAt and Sequence, which seems to work as you requested:

flattenAt[expr_, spec_] := MapAt[Apply@Sequence, expr, spec]

It returns the mostly same results as built-in FlattenAt on standard position specs, and also works with Span via MapAt.

As noted by Mr.Wizard, one place where it differs from the FlattenAt when one wants to Flatten inside held expressions, which the real FlattenAt does, while my version doesn't.

| improve this answer | |
  • $\begingroup$ Could you compare the different answers in terms of performance? Currently I am limited to using the programming cloud and I don't know how representative this is. $\endgroup$ – Sascha Jan 7 '16 at 19:52
  • $\begingroup$ @Sascha Sorry, I am short of time right now. Later, if time permits and no one does that by then. $\endgroup$ – Leonid Shifrin Jan 7 '16 at 19:53
  • $\begingroup$ @LeonidShifrin, it seems not working with degenerate cases like flattenAt[{{a}, {b, c}, {d, e}, {f}}, {{ ;; }, { ;; }}] $\endgroup$ – garej Jan 11 '16 at 21:46
  • $\begingroup$ @garej It is not clear to me how it should work for such a level spec. $\endgroup$ – Leonid Shifrin Jan 11 '16 at 22:21
  • $\begingroup$ @LeonidShifrin. it seems reasonable that it should work the same way as MapAt[h, {a, {b, c}, {d, e}, {f}}, {{ ;; }, { ;; }}] works, where h should behave as it had Flat Attribute. I'm just learning so not able to implement that sort of things. I was able just to use very slow MapIndexed for similar task (you may see modest attempt below). $\endgroup$ – garej Jan 12 '16 at 14:07

I didn't really test how fast this implementation is (feel free to do so and modify if you know something better) but It is as fast as the regular FlattenAt and it handles all* cases of Span.

Clear[flattenSpan, list]

flattenSpan[list_, span_]:=Module[{range},
range = span /. All-> Length@list 
/.{Span[a_,b_] /;b<a :>Table[{i}, {i,a,Length@list+b}],
   Span[a_,b_] :> Table[{i}, {i, a, b}], 
   Span[a_,b_, c_] /; b<a :>Table[{i}, {i, a,Length@list+ b, c}],      
   Span[a_,b_, c_] :>Table[{i}, {i, a, b, c}]};

FlattenAt[list, range]

Examples: for list = {{a}, {b,c}, {d, e}, {f}, {g, h,i}, {j}, {k, l}}

flattenSpan[list, 2;;] (* flattens all elements starting from the second*)
(* {{a},b,c,d,e,f,g,h,i,j,k,l} *) 

flattenSpan[list, ;;;;2]  (* flattens every second element*)
(* {a,{b,c},d,e,{f},g,h,i,{j},k,l} *)

flattenSpan[list, 1;;4;;2] (* flattens every second element between 1 and 4*)
(* {a,{b,c},d,e,{f},{g,h,i},{j},{k,l}} *)

flattenSpan[list, ;;] (* flattens all elements*)
(* {a,b,c,d,e,f,g,h,i,j,k,l} *)

negative indices work as well with one little caveat*: to flatten the last 4 entries in the list one has to use -4;;-1 whereas in MapAt -4;; would suffice.

flattenSpan[list, -4;;-1]       (* flattens the last 4 elements*)
(* {{a},{b,c},{d,e},f,g,h,i,j,k,l} *)

Fixed the issue mentioned in the comment

flattenSpan[list, 1;;-2] (*flattens all elements from 1 to the second-last*)
(* {a,b,c,d,e,f,g,h,i,{j},{k,l}} *)
| improve this answer | |
  • $\begingroup$ try flattenSpan[list, 2 ;; -1] $\endgroup$ – garej Jan 7 '16 at 22:30
  • $\begingroup$ @garej you are right, I included rules to fix that. The behavior for -4;; as explained at the bottom still persists though $\endgroup$ – Sascha Jan 7 '16 at 22:51
  • $\begingroup$ @Sasha, try list = {a, {b,c}, {d, e}, {f} with span ;;2. Seems like FlattenAt weakness not to working with atoms like a persists. $\endgroup$ – garej Jan 9 '16 at 7:57
  • $\begingroup$ @Sasha, try list with atom element, like {a, {b, c}, {}, {{d}, e}, {f}, {a}, {f}} $\endgroup$ – garej Jan 9 '16 at 15:05
  • $\begingroup$ @garej both FlattenAt and Flatten don't handle atoms gracefully. In my humble opinion the implementation should be that Flatten[atom] evaluates to atom. See this question here. $\endgroup$ – Sascha Jan 9 '16 at 16:38

The version of "classic" FlattenAt that does not work with atom elements but works with arguments of MapAt function in different forms:

flatAt[list_, span_Span] := 
  FlattenAt[list, MapIndexed[#2 &, list][[span]]];
flatAt[list_List, span_List /; Length[span] <= 1] := 
  FlattenAt[list, MapIndexed[#2 &, list][[span[[1]]]]];
flatAt[list_, span_List /; Length[span] > 1] := 
   Union @@ 
    Table[MapIndexed[#2 &, list][[span[[i, 1]]]], {i, Length[span]}]];

Examples with sample listok = {{a}, {b, c}, {}, {{d}, e}, {f}, {a}, {f}}:

flatAt[listok, {{-1 ;;}, { ;; }, {;; 2}}]

{a, b, c, {d}, e, f, a, f}

flatAt[listok, {1 ;; }]

{a, b, c, {d}, e, f, a, f}

flatAt[listok, 1 ;; -1]

{a, b, c, {d}, e, f, a, f}

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.