분해는 Array나 Hash 같은 컬렉션에서 원소를 추출하는 것을 말해요.
이렇게 분해한 값은 같은 문 안에서 변수에 할당할 수 있어요.
다중 할당은 한 문 안에서 값을 분해해 여러 변수에 할당하는 기능이에요.
덕분에 코드가 더 간결하고 읽기 쉬워지며, 할당할 변수를 쉼표로 구분해서 쓰면 돼요. 예를 들어 first, second, third = [1, 2, 3]처럼요.
스플랫 연산자(*)와 더블 스플랫 연산자(**)는 분해할 때 자주 쓰여요.
스플랫 연산자(*)는 여러 배열을 하나의 배열로 합칠 때 쓸 수 있어요. 각각을 새로운 공통 배열로 _분해_하는 거예요.
더블 스플랫 연산자(**)는 여러 해시를 하나의 해시로 합칠 때 쓸 수 있어요. 각각을 새로운 공통 해시로 _분해_하는 거예요.
이 문법은 여러 값을 위치 인자로 받는 방식과, 임의의 여러 키워드 인자를 받는 방식을 구분하기 위해 쓰여요.
스플랫 연산자(*)를 컬렉션 없이 사용하면 여러 값을 하나의 배열로 묶어요(또는 조합해요).
이건 다중 할당에서 개별적으로 할당되지 않은 "나머지" 원소를 하나의 변수로 묶을 때 자주 쓰여요.
Ruby에서는 임의의 개수만큼 위치 인자나 키워드 인자를 받는 메서드를 사용하거나 정의할 때 이런 분해/조합 동작을 흔히 써요.
이런 인자는 보통 def some_method(*arguments, **keyword_arguments)처럼 정의하고, some_method(*some_array, **some_hash)처럼 사용해요.
*<variable_name>과 **<variable_name>을 *와 **와 혼동하면 안 돼요.
*와 **는 각각 곱셈과 거듭제곱에 쓰이지만, *<variable_name>과 **<variable_name>은 조합과 분해 연산자로 쓰여요.
다중 할당을 쓰면 여러 변수를 한 줄에서 할당할 수 있어요.
값을 구분하려면 쉼표 ,를 써요:
>> a, b = 1, 2
=> [1, 2]
>> a
=> 1
다중 할당은 한 가지 데이터 타입에만 국한되지 않아요:
>> x, y, z = 1, "Hello", true
=> [1, "Hello", true]
>> x
=> 1
>> y
=> 'Hello'
>> z
=> true
다중 할당으로 배열의 원소를 서로 바꿀 수도 있어요. 정렬 알고리즘에서 특히 흔히 쓰이는 방식이에요. 예를 들어 볼까요:
>> numbers = [1, 2]
=> [1, 2]
>> numbers[0], numbers[1] = numbers[1], numbers[0]
=> [2, 1]
>> numbers
=> [2, 1]
이 방식을 "병렬 할당"이라고도 하는데, 임시 변수를 만들지 않아도 되게 해줘요.
값보다 변수가 많으면 남는 변수에는 nil이 할당돼요:
>> a, b, c = 1, 2
=> [1, 2]
>> b
=> 2
>> c
=> nil
변수보다 값이 많으면 남는 값은 무시돼요:
>> a, b, c = 1, 2, 3, 4
>> b
=> 2
>> c
=> 3
Ruby에서는 배열/해시의 원소를 분해해 각각 다른 변수에 담을 수 있어요. 배열 안의 값은 인덱스 순서대로 있기 때문에, 변수에도 같은 순서로 풀려요:
>> fruits = ["apple", "banana", "cherry"]
>> x, y, z = fruits
>> x
=> "apple"
필요 없는 값이 있다면 _를 써서 "가져왔지만 사용하지 않음"을 나타낼 수 있어요:
>> fruits = ["apple", "banana", "cherry"]
>> _, _, z = fruits
>> z
=> "cherry"
배열 안에 있는 배열(이른바 중첩 배열)에서 값을 분해해 할당하는 것은 얕은 분해와 같은 방식으로 동작하지만, 값의 맥락이나 위치를 분명히 하려면 구분된 분해 표현식 (())이 필요해요:
>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> (a, b), (c, d) = fruits_vegetables
>> a
=> "apple"
>> d
=> "potato"
중첩 배열의 일부만 깊게 풀 수도 있어요:
>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> a, (c, d) = fruits_vegetables
>> a
=> ["apple", "banana"]
>> c
=> "carrot"
분해할 때 변수의 위치가 잘못되었거나 값의 개수가 맞지 않으면 구문 오류가 발생해요:
fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
(a, b), (d) = fruits_vegetables
syntax error, unexpected ')', expecting '.' or &. or :: or '['
((a, b), (d)) = fruits_vegetables
여기서 직접 실험해 보면, 오른쪽에 있는 값이 아니라 첫 번째 패턴이 기준이 된다는 걸 알게 돼요. 구문 오류는 데이터 구조 때문이 아니에요.
*)로 배열 분해하기[배열을 분해할][decomposition] 때 스플랫 연산자(*)를 써서 "남는" 값을 담을 수 있어요.
이 방식이 배열을 잘라내는 것보다 더 명확해요(어떤 상황에서는 잘라내는 쪽이 읽기 어렵기도 해요).
예를 들어 첫 번째 원소를 꺼내고, 나머지 값을 첫 번째 원소가 없는 새로운 배열에 할당할 수 있어요:
>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *last = fruits
>> x
=> "apple"
>> last
=> ["banana", "cherry", "orange", "kiwi", "melon", "mango"]
배열의 처음과 끝에 있는 값만 꺼내고 가운데 값은 전부 묶을 수도 있어요:
>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *middle, y, z = fruits
>> y
=> "melon"
>> middle
=> ["banana", "cherry", "orange", "kiwi"]
깊은 분해에서도 *를 쓸 수 있어요:
>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]]
>> (a, *rest), b = fruits_vegetables
>> a
=> "apple"
>> rest
=> ["banana", "melon"]
Hash 분해하기해시를 분해하는 건 배열을 분해하는 것과 조금 달라요. 해시를 풀려면 먼저 배열로 바꿔야 해요. 그렇게 하지 않으면 분해되지 않아요:
>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory
>> x
=> {:apple=>6, :banana=>2, :cherry=>3}
>> y
=> nil
Hash를 배열로 강제 변환하려면 to_a 메서드를 써요:
>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> fruits_inventory.to_a
=> [[:apple, 6], [:banana, 2], [:cherry, 3]]
>> x, y, z = fruits_inventory.to_a
>> x
=> [:apple, 6]
키를 풀고 싶다면 keys 메서드를 써요:
>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.keys
>> x
=> :apple
값을 풀고 싶다면 values 메서드를 써요:
>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.values
>> x
=> 6
조합은 여러 값을 하나의 배열로 묶어 변수에 할당하는 기능이에요. 값을 _분해_해서 바꾼 다음, 그 결과를 다시 변수로 _조합_하고 싶을 때 유용해요. 또한 두 개 이상의 배열/해시를 병합할 수도 있어요.
*)로 배열 조합하기배열은 스플랫 연산자(*)로 조합할 수 있어요.
모든 값이 하나의 배열로 묶여요.
>> fruits = ["apple", "banana", "cherry"]
>> more_fruits = ["orange", "kiwi", "melon", "mango"]
# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>> combined_fruits = *fruits, *more_fruits
>> combined_fruits
=> ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
**)로 해시 조합하기해시를 조합할 때는 더블 스플랫 연산자(**)를 써요.
한 해시의 모든 키/값 쌍이 다른 해시로 묶이거나, 두 해시가 하나로 합쳐져요.
>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> more_fruits_inventory = {orange: 4, kiwi: 1, melon: 2, mango: 3}
# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}
# then the pairs are packed into combined_fruits_inventory
>> combined_fruits_inventory
=> {:apple=>6, :banana=>2, :cherry=>3, :orange=>4, :kiwi=>1, :melon=>2, :mango=>3}
*)와 더블 스플랫 연산자(**) 사용하기임의의 개수만큼 인자를 받는 메서드를 만들 때는 메서드 정의에 *arguments나 **keyword_arguments를 쓸 수 있어요.
*arguments는 임의의 개수만큼 위치 인자(키워드가 아닌 인자)를 묶을 때 쓰고, **keyword_arguments는 임의의 개수만큼 키워드 인자를 묶을 때 써요.
*arguments 사용하기:
# This method is defined to take any number of positional arguments
# (Using the single line form of the definition of a method.)
>> def my_method(*arguments)= arguments
# Arguments given to the method are packed into an array
>> my_method(1, 2, 3)
=> [1, 2, 3]
>> my_method("Hello")
=> ["Hello"]
>> my_method(1, 2, 3, "Hello", "Mars")
=> [1, 2, 3, "Hello", "Mars"]
**keyword_arguments 사용하기:
# This method is defined to take any number of keyword arguments
>> def my_method(**keyword_arguments)= keyword_arguments
# Arguments given to the method are packed into a dictionary
>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}
정의한 메서드에 키워드 인자를 받을 매개변수(**keyword_arguments나 <key_word>: <value>)가 없다면, 키워드 인자는 하나의 해시로 묶여 마지막 매개변수에 할당돼요.
>> def my_method(a)= a
>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}
*arguments와 **keyword_arguments는 함께 조합해서 쓸 수도 있어요:
def my_method(*arguments, **keyword_arguments)
p arguments.sum
for (key, value) in keyword_arguments.to_a
p key.to_s + " = " + value.to_s
end
end
my_method(1, 2, 3, a: 1, b: 2, c: 3)
6
"a = 1"
"b = 2"
"c = 3"
*arguments 앞뒤에 인자를 써서 특정 위치 인자를 받을 수도 있어요.
배열을 분해할 때와 같은 방식으로 동작해요.
인자는 정해진 순서대로 구성해야 해요:
def my_method(<positional_arguments>, *arguments, <positional_arguments>, <key-word_arguments>, **keyword_arguments)
이 순서를 지키지 않으면 오류가 발생해요.
def my_method(a, b, *arguments)
p a
p b
p arguments
end
my_method(1, 2, 3, 4, 5)
1
2
[3, 4, 5]
*arguments 앞뒤에 위치 인자를 쓸 수도 있어요:
>> def my_method(a, *middle, b)= middle
>> my_method(1, 2, 3, 4, 5)
=> [2, 3, 4]
위치 인자, *arguments, 키워드 인자, **keyword_arguments를 함께 조합할 수도 있어요:
>> def my_method(first, *many, last, a:, **keyword_arguments)
p first
p many
p last
p a
p keyword_arguments
end
>> my_method(1, 2, 3, 4, 5, a: 6, b: 7, c: 8)
1
[2, 3, 4]
5
6
{:b => 7, :c => 8}
인자를 잘못된 순서로 쓰면 오류가 발생해요:
def my_method(a:, **keyword_arguments, first, *arguments, last)
arguments
end
my_method(1, 2, 3, 4, a: 5)
syntax error, unexpected local variable or method, expecting & or '&'
... my_method(a:, **keyword_arguments, first, *arguments, last)
스플랫 연산자(*)를 써서 인자 배열을 메서드 호출에 풀어 넣을 수 있어요:
def my_method(a, b, c)
p c
p b
p a
end
numbers = [1, 2, 3]
my_method(*numbers)
3
2
1
더블 스플랫 연산자(**)를 써서 인자 해시를 메서드 호출에 풀어 넣을 수도 있어요:
def my_method(a:, b:, c:)
p c
p b
p a
end
numbers = {a: 1, b: 2, c: 3}
my_method(**numbers)
3
2
1