イントロ
複数の値をひとまとめにしたコレクションがあったとき、その要素に対して一連の操作を施してひとまとめにしたいことがある。たとえば、全ての要素を順に足すとか:
x = [1,2,3]
val = 0
for in 1:length(x)
val += x[i]
end
これならsum(x)
でよいが、順番を気にしながら処理したいこともあるだろう。たとえば、各要素を順に割りたいとか:
x = [1,2,3]
# 1 / 2 / 3相当が計算したい
val = x[1]
for i in 2:length(x)
val /= x[i]
end # ちょっと長い
println(val) # 0.166666666666666
初期値を決めたいことがあるかもしれない。
x = [1,2,3]
# 1 / 2 / 3相当が計算したい
val = x[1]
for i in 2:length(x)
val /= x[i]
end # ちょっと長い
二つの値をとる任意の関数でやりたいことだってあるかもしれない...
x = [1,2,3]
# f(f(1,2),3)を計算したい。
要素が長くなると面倒くさい。
JuliaのBaseでは、このようにコレクションの要素に対して一連の演算を施して1まとめにするような操作が提供されている。そのなかにBase.reduce
, Base.foldl
, Base.foldr
というのがある。
(裏話: Flux.jlのドキュメントを呼んでたらfoldlが出てきたが、知らんかったのでまとめた)
Base.reduce
reduce(op, itr; [init])
Base.reduceは任意の二項演算を各要素に順に当てていく操作を実現できる。たとえば、上の例は以下のように書ける:
x = [1,2,3]
reduce(+, x) # 6
x = [1,2,3]
reduce(/, x) # 0.1666666666
f(x, y) = (x+y)/2
x = [1,2,3]
reduce(f, x) # ((1+2)/2 + 3)/2 = 2.25
init
で初期値を決めることもできる。空のコレクションに対して何かする場合にはinit必須
x = [1,2,3]
reduce(*, x, init=5) # 30
reduce(*, [], init=5) # 5
reduce(*, []) # error
注意事項1: 特別な場合にはすでに別の関数があるのでそちらをつかう
たとえば、全部足すとかという場合には上でも述べたようにsum(itr)
がある。こういうよく使う例については別途実装されたものがあるのでそちらを使うべきである。このようなものの例には和sum(itr)
、積prod(itr)
、最大最小チェックmaximum(itr)
, minimum(itr)
, and, orall(itr)
, any(itr)
がある。
sum([1,2,3,4]) # 10
prod([1,2,3,4]) # 24
maximum([1,2,3]) # 3
minimum([1,2,3]) # 1
all([true, true, false]) # false
any([true, true, false]) # true
注意事項2: 演算順序が大事な場合はreduceではなくてfoldl, foldrを使う
公式ドキュメントに以下のようにある。
The associativity of the reduction is implementation dependent. This means that you can't use non-associative operations like - because it is undefined whether reduce(-,[1,2,3]) should be evaluated as (1-2)-3 or 1-(2-3). Use foldl or foldr instead for guaranteed left or right associativity.
つまり、演算順序がどうなるかはJuliaの実装依存で、保証がないということである。したがって、たとえば、op
が-
(マイナス)の場合を考えると、これは実装しだいで(1-2)-3
になるかもしれないし、1-(2-3)
になるかもしれない。順序を保証したいときはそれのための関数があるのでそっちを使えというこでとである。
Base.foldl
Base.reduce
のようなものであるが、左の要素から次々と処理されることが保証されている。
(opがf(x,y)だったとすると、yのほうにコレクションの要素が左から順次詰め込まれていく。ただし、最初のxは初期値でコレクションの最後の値か、initの値である)
foldl(=>, 1:3) # (1=>2)=>3
初期値ももちろん与えられる
foldl(=>, 1:3, init=0) # ((0=>1)=>2)=>3
Base.foldr
Base.reduce
のようなものであるが、右の要素から次々と処理されることが保証されている。
(opがf(x,y)だったとすると、xのほうにコレクションの要素が右から順次詰め込まれていく。ただし、最初のyは初期値でコレクションの最後の値か、initの値である)
foldr(=>, 1:3) # (1=>(2=>3))
初期値ももちろん与えられる
foldl(=>, 1:3, init=0) # 1=>(2=>(3=>0))
注意
Setとか順番がよくわからない気がするが、要するにfor ... in iterするときに出てくる順番である。