10

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?

Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
M.R.
  • 31,425
  • 8
  • 90
  • 281
  • 1
    What about something like MapAt[Apply@Sequence, {a, {b, c}, {d, e}, f}, 2 ;; 3]? – Leonid Shifrin Jan 07 '16 at 16:52
  • @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. – Sascha Jan 07 '16 at 19:43
  • @Sascha Ok, why not. Posted. – Leonid Shifrin Jan 07 '16 at 19:49
  • 2
    @Leonid As I'm sure you know Apply will not work inside a held expression so this is not equivalent to FlattenAt. – Mr.Wizard Jan 29 '16 at 01:03
  • 1
    @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. – Leonid Shifrin Jan 29 '16 at 01:16

4 Answers4

6
FlattenSpan[li_, sp_Span] :=
 FlattenAt[li, List /@ Range @@ sp]

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

enter image description here

eldo
  • 67,911
  • 5
  • 60
  • 168
  • 1
    +1. If the list of positions is long, Transpose[{Range@@sp}] will be vastly more efficient than List /@ Range @@ sp. – Leonid Shifrin Jan 07 '16 at 18:11
  • 4
    You could use List /@ Range[Length@li][[sp]] to accommodate All and negative positions in the span. – Simon Woods Jan 07 '16 at 20:55
  • 2
    @Leonid and Partition[Range @@ sp, 1] will be more efficient still. ;-) – Mr.Wizard Jan 29 '16 at 01:02
  • @Mr.Wizard Interesting. Never thought of that. I do use this sort of code in a number of places, so will at some point check the difference and perhaps adopt this method. Thanks for the info. – Leonid Shifrin Jan 29 '16 at 01:05
  • 1
    @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. – Mr.Wizard Jan 29 '16 at 01:08
  • 1
    @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. – Leonid Shifrin Jan 29 '16 at 01:15
6

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

ClearAll[flattenAt];
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.

Leonid Shifrin
  • 114,335
  • 15
  • 329
  • 420
  • 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. – Sascha Jan 07 '16 at 19:52
  • @Sascha Sorry, I am short of time right now. Later, if time permits and no one does that by then. – Leonid Shifrin Jan 07 '16 at 19:53
  • @LeonidShifrin, it seems not working with degenerate cases like flattenAt[{{a}, {b, c}, {d, e}, {f}}, {{ ;; }, { ;; }}] – garej Jan 11 '16 at 21:46
  • @garej It is not clear to me how it should work for such a level spec. – Leonid Shifrin Jan 11 '16 at 22:21
  • @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). – garej Jan 12 '16 at 14:07
  • @garej I see. In this case, one can e.g. define a helper function as follows: ClearAll[fl]; fl[arg_] := Sequence @@ arg; fl[args___] := args;, and then flattenAt[expr_, spec_] := MapAt[fl, expr, spec]. – Leonid Shifrin Jan 12 '16 at 14:11
  • @LeonidShifrin. that is what I was looking for, thank you. – garej Jan 12 '16 at 16:44
5

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}} *)
Sascha
  • 8,459
  • 2
  • 32
  • 66
  • try flattenSpan[list, 2 ;; -1] – garej Jan 07 '16 at 22:30
  • @garej you are right, I included rules to fix that. The behavior for -4;; as explained at the bottom still persists though – Sascha Jan 07 '16 at 22:51
  • @Sasha, try list = {a, {b,c}, {d, e}, {f} with span ;;2. Seems like FlattenAt weakness not to working with atoms like a persists. – garej Jan 09 '16 at 07:57
  • @Sasha, try list with atom element, like {a, {b, c}, {}, {{d}, e}, {f}, {a}, {f}} – garej Jan 09 '16 at 15:05
  • @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. – Sascha Jan 09 '16 at 16:38
  • read OP: "works on the same position arguments as MapAt". what is the logic to have a function that avoid atoms with spans? – garej Jan 09 '16 at 19:14
  • @garej I think my solution gives what the OP asked for. Dealing with atoms inside FlattenAt is another problem entirely and has nothing do to with modifying FlattenAt to take spans as third argument. – Sascha Jan 09 '16 at 22:53
  • I like your solution but it does not. even without atoms. Compare: MapAt[h, {{a}, {b, c}, {d, e}, {f}}, 1 ;; -1] == MapAt[h, {{a}, {b, c}, {d, e}, {f}}, ;;] and yours flattenSpan[{{a}, {b, c}, {d, e}, {f}}, 1 ;; -1] == flattenSpan[{{a}, {b, c}, {d, e}, {f}}, ;;]. may be you will see what I was trying to say ;)) anyway, let us stop for now... – garej Jan 09 '16 at 23:09
3

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] := 
  FlattenAt[list, 
   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}

garej
  • 4,865
  • 2
  • 19
  • 42