Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

루비 실력을 한 단계 끌어올려 줄 흥미로운 코드 예제 7가지

새로운 루비 트릭, 패턴, 메서드를 배우는 가장 확실한 방법 중 하나는 다른 개발자들이 작성한 코드를 읽는 것입니다.

하지만 읽을 가치가 있는 흥미로운 코드는 어디에서 찾을 수 있을까요?

다행히 오픈소스 프로젝트와 코딩 챌린지 사이트가 많습니다. 특히 코딩 챌린지 사이트는 자신의 답안을 제출한 후 다른 개발자들의 풀이를 열람할 수 있어 학습에 큰 도움이 됩니다.

이 글에서 다룰 내용:

저는 여러분이 즐겁게 읽을 만한 7가지 코드 예제를 엄선했습니다. 각 예제마다 해설을 곁들여 최대한 많은 것을 얻어 가실 수 있도록 안내해 드리겠습니다.

두 수의 합 구하기

이 예제에서는 고유한 숫자로 이루어진 배열이 주어졌을 때, 두 숫자를 더했을 때 목표 숫자(target)가 되는 조합이 존재하는지 확인하는 방법을 알아봅니다.

코드:

def sum_eq_n?(arr, n)
  return true if arr.empty? && n == 0

  arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }
end

이 코드가 흥미로운 이유는 product 메서드를 활용했기 때문입니다.

product 메서드를 사용하면 마치 반복문 안에 반복문을 넣은 것처럼 배열 A의 모든 값과 배열 B의 모든 값을 조합할 수 있습니다.

카운팅, 매핑 그리고 찾기

(2, 4, 6, 10)처럼 등차수열에서 빠진 숫자를 찾고 싶다고 가정해 보겠습니다.

여기서는 숫자들 사이의 차이를 계산하는 전략을 사용할 수 있습니다.

[2, 2, 4]

우리의 목표는 이 수열이 어떤 패턴을 따르는지 파악하는 것입니다.

증가하는 수열인가요, 감소하는 수열인가요?

얼마나씩 변하나요?

다음 코드가 수열의 규칙을 밝혀 줍니다:

differences = [2, 2, 4]

differences.max_by { |n| differences.count(n) }
# 2
# 이것이 수열 내 숫자들 사이의 증가폭입니다

수열의 규칙을 알아냈다면, 모든 숫자를 비교해 빠진 숫자를 찾을 수 있습니다.

코드는 다음과 같습니다:

def find_missing(sequence)
  consecutive     = sequence.each_cons(2)
  differences     = consecutive.map { |a,b| b - a }
  sequence        = differences.max_by { |n| differences.count(n) }

  missing_between = consecutive.find { |a,b| (b - a) != sequence }

  missing_between.first + sequence
end

find_missing([2,4,6,10])
# 8

여기서 우리 대신 힘든 일을 처리해 주는 루비 메서드가 몇 개나 사용되었는지 한번 세어 보세요 🙂

정규표현식 예제

문자열을 다루면서 특정 패턴을 찾고 싶다면 정규표현식이 든든한 동반자가 되어 줄 것입니다.

다소 까다롭게 느껴질 수 있지만, 꾸준히 연습하면 완벽하게 익힐 수 있습니다!

자, 그럼 시작해 보겠습니다:

주어진 문자열이 "모음 → 비모음" 문자가 번갈아 나타나는 패턴을 따르는지 확인하고 싶다고 해봅시다.

예를 들면 이렇습니다:

"ateciyu"

이럴 때 정규표현식과 match? 메서드를 함께 사용하면 됩니다.

코드 예제입니다:

def alternating_characters?(s)
  type = [/[aeiou]/, /[^aeiou]/].cycle

  if s.start_with?(/[^aeiou]/)
    type.next
  end

  s.chars.all? { |ch| ch.match?(type.next) }
end

alternating_characters?("ateciyu")
# true

주목할 점 몇 가지:

  1. cycle 메서드를 사용하면 모음 정규표현식과 비모음 정규표현식 사이를 계속 전환할 수 있습니다.
  2. chars 메서드로 문자열을 문자 배열로 변환하면 all? 메서드를 활용할 수 있습니다.

재귀와 스택 예제

재귀(recursion)란 메서드가 스스로를 여러 번 호출하면서 해답에 한 걸음씩 다가가는 기법입니다.

흥미로운 많은 문제들이 재귀로 해결될 수 있습니다.

하지만 재귀에는 호출 깊이 제한이 있기 때문에, 상황에 따라 스택(stack) 자료구조를 대신 활용할 수도 있습니다.

이번 예제를 살펴보겠습니다:

주어진 배열의 "멱집합(Power Set)"을 구하는 문제입니다. 멱집합이란 배열에서 만들 수 있는 모든 부분집합의 집합을 의미합니다.

먼저 재귀를 사용한 예제입니다:

def get_numbers(list, index = 0, taken = [])
  return [taken] if index == list.size

  get_numbers(list, index+1, taken) +
  get_numbers(list, index+1, taken + [list[index]])
end

get_numbers([1,2,3])

같은 문제를 스택으로 해결한 코드입니다:

def get_numbers_stack(list)
  stack  = [[0, []]]
  output = []

  until stack.empty?
    index, taken = stack.pop

    next output << taken if index == list.size

    stack.unshift [index + 1, taken]
    stack.unshift [index + 1, taken + [list[index]]]
  end

  output
end

핵심 아이디어는 알고리즘의 각 단계에서 숫자를 "취하거나" "취하지 않거나", 둘 중 하나를 선택한다는 것입니다.

양쪽 경우를 모두 분기해 시도함으로써 가능한 모든 조합을 만들어 냅니다.

나무(tree)를 상상해 보세요. 각 잎사귀가 하나의 해답입니다.

눈여겨볼 점 몇 가지:

  1. 재귀를 사용한 해법이 더 짧습니다.
  2. 알고리즘이 실제로 "진행"되는 부분(index + 1)은 거의 동일합니다.
  3. 루비에는 Stack 클래스가 없기 때문에, 여기서 사용한 스택은 그저 배열일 뿐입니다.

메서드 체이닝 예제

이것은 제가 가장 좋아하는 예제입니다. 루비가 얼마나 강력한 언어인지 보여 주기 때문입니다.

메서드를 조합하면 한 메서드의 출력 결과를 다른 메서드의 입력으로 전달할 수 있습니다.

마치 공장 생산 라인과 같습니다!

원자재(입력값)로 시작해 메서드를 호출하는 과정을 거치며, 원자재를 원하는 결과물로 서서히 변형시키는 것이죠.

예제를 보겠습니다:

def longest_repetition(string)
  max = string
          .chars
          .chunk(&:itself)
          .map(&:last)
          .max_by(&:size)

  max ? [max[0], max.size] : ["", 0]
end

longest_repetition("aaabb")
# ["a", 3]

문자열이 주어지면 이 코드는 가장 길게 반복된 문자를 찾아 냅니다.

참고할 점:

  • 가독성을 극대화하도록 코드가 정렬된 방식
  • Symbol#to_proc 패턴(&:size)의 활용

참고로, 이것을 "데메테르의 법칙(Law of Demeter)"과 혼동하지 마세요.

그 법칙은 다른 객체의 내부까지 손을 뻗는 것에 관한 것입니다.

여기서는 객체를 변형할 뿐입니다.

with_index 예제

컬렉션을 순회하면서 현재 인덱스도 함께 얻고 싶으신가요?

with_index 메서드를 사용하면 됩니다.

예제입니다:

def reverse_alternate(string)
  string.gsub(/[^\s]+/).with_index { |w, idx| idx.even? ? w : w.reverse }
end

reverse_alternate("Apples Are Good")
# "Apples erA Good"

주목할 점:

  • with_indexeven?을 조합해 현재 단어를 뒤집어야 하는지 판단합니다.
  • 블록 없이 호출한 gsubEnumerator 객체를 반환하므로, 다른 메서드와 체이닝할 수 있습니다.

each_with_object 예제

또 하나 흥미로운 메서드는 each_with_object이며, 그 친구격인 with_object도 있습니다.

결과를 담아 둘 객체가 필요할 때 이 두 메서드를 사용할 수 있습니다.

예제입니다:

def clean_string(str)
  str
    .chars
    .each_with_object([]) { |ch, obj| ch == "#" ? obj.pop : obj << ch }
    .join
end

clean_string("aaa#b")

이 예제에서는 # 기호를 발견하면 마지막 문자를 삭제하고 싶습니다.

주목할 점:

  • each_with_object는 인수를 받는데, 이 인수는 결과를 담기 시작할 객체입니다. 이 인수는 블록의 두 번째 매개변수가 됩니다.
  • 문자열을 문자 배열로 변환한(chars) 후, 작업이 끝나면 다시 문자열로 되돌립니다(join).
  • 삼항 연산자로 분기를 처리해 코드를 더 짧게 유지했습니다.

마무리

이 글에서는 여러분이 더 나은 루비 코드를 작성하는 데 도움이 되도록, 해설과 설명을 곁들인 흥미로운 코드 예제들을 살펴보았습니다.

더 많은 사람들이 이 글을 즐길 수 있도록 공유하는 것도 잊지 마세요.

읽어 주셔서 감사합니다!