In-place mapslices?

I want to apply a function that operates on a vector, to each column of a matrix. So, I use mapslices(). But, how to modify the matrix in-place?

The following is a failed attempt:

function f!(xs)
  a = xs[1]
  @. xs = xs / a
end

xs = Float64.([3,4,5])
f!(xs)
display(xs) # Elements of xs have been changed

xs = Float64.(hcat([3,4,5],[6,7,8]))
ys = mapslices(f!, xs, dims=1)
display(xs) # -> Not changed
display(ys)

xs = Float64.(hcat([3,4,5],[6,7,8]))
@views ys = mapslices(f!, xs, dims=1)
display(xs) # -> Not changed
display(ys)

Even the @views macro doesn’t seem to work.

I wonder why mapslices() doesn’t pass a view ? Even if the function doesn’t change the contents of the array, a view wouldn’t change the results . . . or would it?

1 Like

Don’t know if your question is about mapslices more generally. But in case your question is about how to achieve the result with a simple syntax, you could use

foreach(f!, eachcol(xs))

As far as I remember, mapslices wasn’t still polished and is less performant than other options. For example, using sum in the following example is more performant:

using BenchmarkTools
xs = rand(100,100)

@btime mapslices(sum, $xs; dims=1)  # 4.836 μs (15 allocations: 2.125 KiB)
@btime sum($xs; dims=1)             # 767.667 ns (1 allocations: 896 bytes)
4 Likes

I would expect mapslices, like map, to return a new Array, which in turn sort of implicitly implies that you don’t want to mutate the original data.

In any case, the fact that mapslices does not operate on views and does not mutate the input, is explicitly mentioned in the docs

help?> mapslices
...
  Notice that in eachslice(A; dims=2), the specified dimension is the one without a colon in the slice. This is
  view(A,:,i,:), whereas mapslices(f, A; dims=(1,3)) uses A[:,i,:]. The function f may mutate values in the slice
  without affecting A.

Thanks. In that case, I need to change my example. In my actual program, I work on 2D slices of a 3D array:

function f!(arr2D) #
  a = arr2D[1,1]
  @. arr2D = arr2D / a
end
arr3D = rand(3, 4, 5)
mapslices(f!, arr3D, dims=3)

I’m reading the documentation of foreach and I think I need to be able to create an iterator that gives the 2D slices: arr3D[:, :, 1], arr3D[:, :, 2], . . . and I guess it’s eachslice() . . . So,

function f!(arr2D) #
  a = arr2D[1,1]
  @. arr2D = arr2D / a
end
arr3D = rand(4, 3, 2)
foreach(f!, eachslice(arr3D, dims=3))

which works!

So, I wonder what the raison d’être of mapslices() is, then . . . foreach() is more general because it can work on any iterator. Perhaps, mapslices() gains more performance by assuming that its argument is an array?

I did and do expect that, too, but I also expect that mapslices! existed!

I’ve just searched this forum for map!() and found a discussion thread about some confusion and difficulty.

There must be something I don’t understand. I thought that map and map! were trivial and likewise mapslices and mapslices! were trivial, just like the pair of read() and read!().

To clarify, should it be helpful, map! is also not intended to alter the input: you have to specify another collection destination, distinct from the input.

julia> x = rand(3); y = similar(x); x
3-element Vector{Float64}:
 0.09030089058338875
 0.6696011035030889
 0.33002505704684126

julia> map!(a -> 2a, y, x);

julia> x
3-element Vector{Float64}:
 0.09030089058338875
 0.6696011035030889
 0.33002505704684126

julia> y
3-element Vector{Float64}:
 0.1806017811667775
 1.3392022070061778
 0.6600501140936825

In principle you could use

julia> map!(a -> 2a, x, x); x
3-element Vector{Float64}:
 0.1806017811667775
 1.3392022070061778
 0.6600501140936825

but the documentation explicitly warns against this:

help?> map!
...
  │ Warning
  │
  │  Behavior can be unexpected when any mutated argument shares memory with any other argument.

( You could use broadcast! in this case

julia> broadcast!(a -> 2a, x, x)
3-element Vector{Float64}:
 0.361203562333555
 2.6784044140123555
 1.320100228187365

which is equivalent to

julia> x .= 2 .* x
3-element Vector{Float64}:
 0.72240712466711
 5.356808828024711
 2.64020045637473

.)

Note that in this example we are using a non-mutating function (f = a -> 2a). If you have a mutating function f!, map(f!, ...) and map!(f!, ...) make little sense since we don’t want to store the outputs* of f! into some (new) collection, we just want to apply f! everywhere. (*In fact, it makes perfect sense to make f! return nothing.) To apply f! to each element, use foreach, cf. @alfaromartino 's reply.

Alternatively, you could of course also just use a simple for-loop, which to me seems the clearest and easiest option:

function f!(arr2D)
  a = arr2D[1,1]
  @. arr2D = arr2D / a
end

arr3D = rand(2, 2, 3)
for i = axes(arr3D, 3)
    f!(@view arr3D[:, :, i])
end
#= arr3D now contains e.g.
2×2×3 Array{Float64, 3}:
[:, :, 1] =
 1.0      2.10948
 6.01437  6.37489

[:, :, 2] =
 1.0         1.16381
 0.00573419  1.19772

[:, :, 3] =
 1.0       0.0260569
 0.403835  0.511961
=#

The natural alternative would be map(..., eachslice(...)). But this is not equivalent to mapslices(..., ...) as the former works on views, and the latter on copies.

function f!(xs)
  a = xs[1]
  @. xs = xs / a
  # (Note that f! implicitly returns xs)
end

xs = Float64.(hcat([3,4,5],[6,7,8]))
ys = mapslices(f!, xs, dims=1)
display(xs) # -> Not changed

xs = Float64.(hcat([3,4,5],[6,7,8]))
ys = map(f!, eachslice(xs, dims=1))
display(xs) # -> Changed
# (By the way, ys here consists of views of (the mutated) xs. I.e if you alter xs, ys is also changed.)
# Don't use map in this way though, as this is not how it's supposed to be used.