기관사

기관사

학습 연습 문제

소개

분해는 Array나 Hash 같은 컬렉션에서 원소를 추출하는 것을 말해요. 분해한 값은 같은 문 안에서 변수에 할당할 수 있어요.

다중 할당은 하나의 문 안에서 여러 변수에 분해된 값을 할당하는 기능이에요. 덕분에 코드를 더 간결하고 읽기 좋게 만들 수 있어요. 할당할 변수들을 쉼표로 구분해서 쓰면 되는데, 예를 들어 first, second, third = [1, 2, 3]처럼 쓰면 돼요.

스플랫 연산자(*)와 이중 스플랫 연산자(**)는 분해할 때 자주 사용해요.

Caution

*<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]
Note

이것은 "병렬 할당"이라고도 하며, 임시 변수를 만들지 않아도 되게 해줘요.

값보다 변수가 더 많으면, 남는 변수에는 nil이 할당돼요:

>> a, b, c = 1, 2
=> [1, 2]
>> b
=> 2
>> c
=> nil

분해

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
# syntax error, unexpected ')', expecting '.' or &. or :: or '['

여기서 직접 실험해 보면, 오른쪽에 있는 값이 아니라 첫 번째 패턴이 기준을 정한다는 걸 알게 될 거예요. 이 구문 오류는 데이터 구조와는 상관이 없어요.

단일 스플랫 연산자(*)로 배열 분해하기

배열을 분해할 때 스플랫 연산자(*)를 사용해서 "남은" 값들을 잡을 수 있어요. 이 방법이 배열을 잘라내는 것보다 더 명확해요(어떤 상황에서는 잘라내는 쪽이 덜 읽기 좋거든요). 예를 들어, 첫 번째 원소를 추출하고 나머지 값을 첫 번째 원소가 없는 새 배열에 할당할 수 있어요:

>> 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 앞뒤에 인자를 써서 특정 위치의 인자를 지정할 수도 있어요. 이는 배열을 분해하는 것과 같은 방식으로 동작해요.

Caution

인자는 정해진 순서대로 구성해야 해요:

def my_method(<positional_arguments>, *arguments, <positional_arguments>, <keyword_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

지침

친구 Linus는 도시 사이에서 화물 열차를 운전하는 기관사예요. Linus는 열차를 다루는 일은 정말 잘하지만, 물류나 컴퓨터를 다루는 일은 잘하지 못해요. 그래서 열차 정보를 정리하고 경로 데이터의 실수를 바로잡는 일에 프로그래밍 도움을 받고 싶어 해요.

Note

이 연습 문제는 슬라이싱, 인덱싱, 그리고 다양한 hash 메서드를 사용하면 쉽게 풀 수 있어요. 하지만 아래 각 과제를 풀 때에는 패킹, 언패킹, 다중 할당을 연습해 보는 것이 목표예요.

1. 모든 화차를 담은 배열 만들기

Linus는 화차 식별자(ID)를 하나하나 기록해 왔지만, 시스템이 한 번에 몇 대의 화차를 처리해야 할지는 전혀 알 수 없어요. 이 데이터를 하나의 배열로 묶어 두면 나머지 물류 프로그램이 훨씬 쉽게 처리할 수 있어요.

임의의 개수만큼 화차 ID를 받는 generate_list_of_wagons() 메서드를 구현해요. 각 ID는 양의 정수예요. 그런 다음 메서드는 주어진 ID들을 하나의 배열로 return해야 해요.

LocomotiveEngineer.generate_list_of_wagons(1, 7, 12, 3, 14, 8, 5)
# => [1, 7, 12, 3, 14, 8, 5]

2. 화차 배열 바로잡기

이쯤 되면 데이터와 물류 프로그램에서 데이터가 어떻게 쓰이는지 감이 잡히기 시작할 거예요. ID 시스템은 항상 기관차에 1이라는 ID를 부여하고, 열차의 나머지 화차에는 1보다 큰 임의의 ID를 부여해요.

Linus는 열차에 새 화차 두 대를 연결하고는 시스템을 업데이트하는 걸 잊었어요! 이제 열차 배열의 맨 앞 두 화차를 끝으로 옮겨야 해요. 그렇지 않으면 모든 순서가 엉망이 돼요.

게다가 Linus는 빠진 화차 ID가 들어 있는 것 같은 두 번째 배열을 방금 발견했어요. 새 화차를 옮기고 나면, 이 두 번째 배열의 ID들을 지정된 기관차 바로 뒤에 놓아야 한다는 것만 기억하고 있어요.

Linus는 실수를 고치고 데이터를 하나로 합쳐 주면 정말 고마워할 거예요.

화차 ID가 담긴 두 개의 배열을 받는 fix_list_of_wagons() 메서드를 구현해요. 이 메서드는 첫 번째 배열의 처음 두 항목을 끝으로 옮기고, 두 번째 배열의 값들을 기관차 ID(1) 뒤(오른쪽)에 삽입해야 해요. 그런 다음 메서드는 수정된 배열을 return해야 해요.

LocomotiveEngineer.fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15])
# => [1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5]

3. 빠진 정차역 추가하기

이제 화차 데이터가 모두 올바르니, Linus는 시스템의 경로 정보를 업데이트해 주기를 바라요. 운송 경로를 따라가다 보면, 열차가 화물을 싣거나 내리기 위해 여러 역에 정차할 수 있어요. 여정마다 이러한 중간 배송 지점의 개수가 다를 수 있어요. Linus는 빠졌거나 추가된 배송 정보로 시스템의 경로 해시를 업데이트해 주기를 바라요.

경로 해시와 그 뒤에 오는 가변 개수의 키워드 인자를 받는 add_missing_stops() 메서드를 구현해요. 이 인자들은 하나 이상의 정차역을 담은 해시 형태일 수도 있고, stop_<number>: "city" 형태의 키워드 쌍 여러 개일 수도 있어요. 그런 다음 메서드는 추가된 모든 정차역을 순서대로 담은 키가 더해진 경로 해시를 반환해야 해요.

LocomotiveEngineer.add_missing_stops({from: "New York", to: "Miami"},
                    stop_1: "Washington, DC", stop_2: "Charlotte", stop_3: "Atlanta",
                    stop_4: "Jacksonville", stop_5: "Orlando")
# => {from: "New York", to: "Miami", stops: ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]}
Note

정차역은 항상 올바른 순서로 전달되니, 정렬은 걱정하지 않아도 돼요.

4. 경로 정보 확장하기

Linus는 경로 프로그램을 작업하다가 일부 경로에 중요한 세부 정보가 빠져 있다는 걸 알아챘어요. 초기 경로 정보는 해시로 만들어져 있는데, Linus는 빠진 부분이 있으면 그 해시를 업데이트해 주기를 바라요. 시스템의 모든 경로는 조금씩 다른 세부 정보를 필요로 해서, Linus는 범용적인 해결책을 정말 원해요.

두 개의 해시를 받는 extend_route_information() 메서드를 구현해요. 첫 번째 해시에는 열차 경로가 오가는 출발지와 도착지 도시가 들어 있어요.

두 번째 해시에는 열차 속도, 길이, 온도 같은 다른 경로 세부 정보가 들어 있어요. 메서드는 모든 경로 정보를 하나로 합친 해시를 반환해야 해요.

Note

두 번째 해시는 예시에 나온 것과 다르거나 더 많은 속성을 담을 수 있어요.

LocomotiveEngineer.extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
# => {"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Ruby Exercism

기관사 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Ruby 트랙을 개념 21개연습 문제 121개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.