Pythonを使ったことがある人は、ほぼ間違いなくリスト内包表記を使ったことがあるでしょう。リスト内包表記は、初期のバージョンからPythonの構文の中心的な存在です。
これほど便利な仕組みは、やがてJuliaをはじめとする他の言語にも取り入れられていきます。
Juliaのプログラマーにとって、内包表記は必須ではなく選択肢のひとつです。たいていは代わりの書き方(ブロードキャスト、高階関数など)があります。
しかし、配列を組み立てるのに、内包表記がシンプルで読みやすく、しかも高速な方法になることもよくあります。 使うかどうかは、結局のところ好みの問題であり、Pythonと関数型言語のどちらが好みかにもよります。
構文はほぼPythonからの直輸入ですが、多次元配列のための拡張が加えられています。
省略可能なif節も含め、とてもPythonらしい書き方です。
julia> [x^2 for x in 1:3]
3-element Vector{Int64}:
1
4
9
julia> [x^2 for x in 1:3 if isodd(x^2)]
2-element Vector{Int64}:
1
9
出力の形は内包表記の書き方によって変わります。単純な場合はふつう入力のコレクションと同じ形になりますが、if節を付けると1次元のVectorに平坦化されます。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> [x^2 for x in m]
2×3 Matrix{Int64}:
1 4 9
16 25 36
julia> [x^2 for x in m if isodd(x^2)]
3-element Vector{Int64}:
1
25
9
Pythonと同じように、for節を複数書いてそれぞれ別の変数を扱えます。入力のコレクションは同じでも違ってもかまいません。
julia> [x * y for x in 1:3 for y in 4:6]
9-element Vector{Int64}:
4
5
6
8
10
12
12
15
18
julia> [x * y for x in 1:3 for y in 4:6 if isodd(x * y)]
2-element Vector{Int64}:
5
15
これは入れ子のループと同じです。 入力が行列でも、出力は1次元になります。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> [x*y for x in m for y in m]
36-element Vector{Int64}:
1
4
2
5
(truncated output...)
前のセクションでは、スペースだけで区切った複数のfor節を説明しました。
このセクションでは、forは1つだけで、変数はカンマで区切ります。
julia> [(x, y) for x in 1:3, y in 4:6]
3×3 Matrix{Tuple{Int64, Int64}}:
(1, 4) (1, 5) (1, 6)
(2, 4) (2, 5) (2, 6)
(3, 4) (3, 5) (3, 6)
内包表記の各変数が、出力に新しい次元を作ります。変数の組み合わせそれぞれに対応する要素が入ります。
上の例では、xは行方向に、yは列方向に増えていきます。
より高い次元も可能ですが、出力が読みにくくなるといういつもの注意点があります。
ここまでは、内包表記を括弧([ ... ])で囲んだ、配列を出力する場合を中心に見てきました。
この括弧を丸括弧(( ... ))に置き換えると、結果は変わります。得られるのはgenerator expressionです。これは遅延評価されるイテレーターで、必要になったときに次の値を返します。
julia> g = ((x, y) for x in 1:3, y in 4:6)
Base.Generator{Base.Iterators.ProductIterator{Tuple{UnitRange{Int64}, UnitRange{Int64}}}, var"#9#10"}(var"#9#10"(), Base.Iterators.ProductIterator{Tuple{UnitRange{Int64}, UnitRange{Int64}}}((1:3, 4:6)))
# Indexing fails with a generator, the entries don't exist yet
julia> g[1, 2]
ERROR: MethodError: no method matching getindex(::Base.Generator{Base.Iterators.ProductIterator{Tuple{UnitRange{…}, UnitRange{…}}}, var"#9#10"}, ::Int64, ::Int64)
# conversion to array
julia> collect(g)
3×3 Matrix{Tuple{Int64, Int64}}:
(1, 4) (1, 5) (1, 6)
(2, 4) (2, 5) (2, 6)
(3, 4) (3, 5) (3, 6)
# generators are mainly designed for iteration
julia> [i * j for (i, j) in g]
3×3 Matrix{Int64}:
4 5 6
8 10 12
12 15 18
内包表記の結果をそのまま次の処理に使う場合、ジェネレーターを使うとメモリを節約できます。大きな中間配列を保持する必要がないからです。
ジェネレーターを関数の引数として使うときは、余分な括弧は必要ありません。
# inefficient
julia> v = [x^2 for x in 1:1e6];
julia> sum(v)
3.333338333335e17
# better - use a generator
julia> sum(x^2 for x in 1:1e6)
3.3333383333312755e17
ジェネレーターの構文は、周りを囲む括弧が違うだけで、内包表記とまったく同じです。
ジェネレーターは、辞書のコンストラクターでも便利に使えます。
julia> Dict(x => x^2 for x in 1:5)
Dict{Int64, Int64} with 5 entries:
5 => 25
4 => 16
2 => 4
3 => 9
1 => 1
オンラインのファッションブティックで働いているとします。 ユーザーにコーディネートを提案するウェブサイトの新機能を思いつきます。 ここで注目したいのは、候補となるコーディネート、その価格、そして色がぶつかる組み合わせを取り除くことです。
以下の問題にはいろいろな解き方がありますが、どれも内包表記で解くことができます。
clothingitem()を実装しましょう。2つのTupleを引数に取ります。1つはcategories、もう1つはそれぞれに対応するqualitiesです。
この関数は、categoriesをキー、qualitiesを値とするDictを返します。
julia> categories = ("item_name", "price", "color", "base_color");
julia> qualities = ("Descriptive Name", 99.00, "Ochre Red", "red");
julia> clothingitem(categories, qualities)
Dict{String, Any} with 4 entries:
"price" => 99.0
"item_name" => "Descriptive Name"
"base_color" => "red"
"color" => "Ochre Red"
categoriesの数はいくつでも扱えるようにしましょう。
get_combinations()を実装しましょう。トップスのTupleとボトムスのTupleを引数に取ります。
この関数は、2つのタプルのcartesian productを2次元配列(すなわちMatrix)として返します。
各要素は(top, bottom)の組み合わせをTupleにしたものです。
行方向にトップスが変わり、列方向にボトムスが変わります。
julia> tops = (
Dict("item_name" => "Dress shirt"),
Dict("item_name" => "Casual shirt")
);
julia> bottoms = (
Dict("item_name" => "Jeans"),
Dict("item_name" => "Dress trousers")
);
julia> get_combinations(tops, bottoms)
2×2 Matrix{Tuple{Dict{String, String}, Dict{String, String}}}:
(Dict("item_name"=>"Dress shirt"), Dict("item_name"=>"Jeans")) (Dict("item_name"=>"Dress shirt"), Dict("item_name"=>"Dress trousers"))
(Dict("item_name"=>"Casual shirt"), Dict("item_name"=>"Jeans")) (Dict("item_name"=>"Casual shirt"), Dict("item_name"=>"Dress trousers"))
衣料品にはそれぞれpriceフィールドが関連付けられているので、コーディネート全体の価格を比較できると便利です。
get_prices()を実装しましょう。これはコーディネートの組み合わせの配列を引数に取ります。
この関数は、それぞれの位置に対応するコーディネートの価格を格納した、同じ形の配列を返します。
julia> tops = (
Dict("item_name" => "Dress shirt", "base_color" => "blue", "price" => 35),
Dict("item_name" => "Casual shirt", "base_color" => "black", "price" => 20)
);
julia> bottoms = (
Dict("item_name" => "Jeans", "base_color" => "blue", "price" => 30),
Dict("item_name" => "Dress trousers", "base_color" => "black", "price" => 75)
);
julia> combomatrix = get_combinations(tops, bottoms);
julia> get_prices(combomatrix)
2×2 Matrix{Int}::
65 110
50 95
julia> filteredmatrix = filter_clashing(combomatrix)
1-element Vector{Tuple{Dict{String, Any}, Dict{String, Any}}}:
(Dict("price" => 20, "item_name" => "Casual shirt", "base_color" => "black"), Dict("price" => 30, "item_name" => "Jeans", "base_color" => "blue"))
julia> get_prices(filteredmatrix)
1-element Vector{Int}:
50
衣料品にはそれぞれbase_colorフィールドがあります。
このフィールドを使って、トップスとボトムスのベースカラーが同じ組み合わせをすべて取り除きます。
filter_clashing()を実装しましょう。コーディネートの組み合わせの配列を引数に取ります。
該当する組み合わせを1次元配列(すなわちVector)で返します。
julia> tops = (
Dict("item_name" => "Dress shirt", "base_color" => "blue", "price" => 35),
Dict("item_name" => "Casual shirt", "base_color" => "black", "price" => 20)
);
julia> bottoms = (
Dict("item_name" => "Jeans", "base_color" => "blue", "price" => 30),
Dict("item_name" => "Dress trousers", "base_color" => "black", "price" => 75)
);
julia> combos = get_combinations(tops, bottoms);
julia> filter_clashing(combos)
1-element Vector{Tuple{Dict{String, Any}, Dict{String, Any}}}:
(Dict("price" => 20, "item_name" => "Casual shirt", "base_color" => "black"), Dict("price" => 30, "item_name" => "Jeans", "base_color" => "blue"))