Short answer
Use
@views array[:, :, 2][9] = 999
More details
The thing to know here is that x[1] = y is lowered to a single function call to setindex!, no matter what x is. This is a normal function, and so all of its arguments are evaluated before the function is called. Here is how you can see what is being done:
julia> set1(x) = x[1] = 33
set1 (generic function with 1 method)
julia> @code_lowered set1(zeros(3))
CodeInfo(
1 ─ Base.setindex!(x, 33, 1)
└── return 33
)
julia> :( z[1,:][2] ) # the brackets show precedence, else try |> dump
:((z[1, :])[2])
julia> set2(z) = z[1,:][2] = 3^4;
julia> @code_lowered set2(zeros(2,3))
CodeInfo(
1 ─ %1 = Core.apply_type(Base.Val, 4)
│ %2 = (%1)()
│ %3 = Base.literal_pow(Main.:^, 3, %2)
│ %4 = Base.getindex(z, 1, Main.:(:))
│ Base.setindex!(%4, %3, 2)
└── return %3
)
When x is another indexing expression, this is just the function call getindex(z,1,:). That makes a copy, and setindex! writes into the copy, and then returns its right hand side (the temporary variable %3, containing 3^4). The copy never escapes.
The way to get the behaviour you want is to make x a view not a copy, by writing view(z,1,:)[2] = 33 or:
julia> set3(z) = @views z[1,:][2] = 33;
julia> @code_lowered set3(zeros(2,3))
CodeInfo(
1 ─ %1 = (Base.maybeview)(x, 1, Main.:(:))
│ Base.setindex!(%1, 33, 2)
└── return 33
)
julia> z3 = zeros(2,3);
julia> set3(z3)
33
julia> z3
2×3 Matrix{Float64}:
0.0 33.0 0.0
0.0 0.0 0.0
It would certainly have been possible to lower z[1,:][2] = 3 to something else, some function like multi_setindex!(z, 3, (1,:), (2,)) perhaps. I guess that was judged to be too much complication.
I think that R has non-standard evaluation, under which a function can reach into its arguments before they are evaluated, but Julia doesn't allow this. It does have macros (always denoted with @) which get to act on the input syntax before evaluation. This is necessary for @views to work, and of course for @code_lowered to see something other than 33 or 9.