가능성의 배열: 루비 패턴 매칭 가이드

게시 됨: 2022-03-11

패턴 일치는 Ruby 2.7에 새로 추가된 기능입니다. 관심 있는 사람이라면 누구나 Ruby 2.7.0-dev를 설치하고 확인할 수 있도록 트렁크에 커밋되었습니다. 이들 중 어느 것도 확정되지 않았으며 개발 팀에서 피드백을 찾고 있으므로 피드백이 있는 경우 기능이 실제로 출시되기 전에 커미터에게 알릴 수 있습니다.

이 글을 읽으신 후 패턴 매칭이 무엇인지, 루비에서 어떻게 사용하는지 이해하셨으면 합니다.

패턴 매칭이란?

패턴 매칭은 함수형 프로그래밍 언어에서 흔히 볼 수 있는 기능입니다. Scala 문서에 따르면 패턴 일치는 "패턴에 대해 값을 확인하는 메커니즘입니다. 성공적인 일치는 가치를 구성 요소로 분해할 수도 있습니다.”

이것은 Regex, 문자열 일치 또는 패턴 인식과 혼동되어서는 안 됩니다. 패턴 일치는 문자열과 관련이 없지만 대신 데이터 구조입니다. 제가 패턴 매칭을 처음 접한 것은 약 2년 전 Elixir를 사용했을 때였습니다. 나는 Elixir를 배우고 그것으로 알고리즘을 풀려고 했습니다. 내 솔루션을 다른 솔루션과 비교하고 패턴 일치를 사용하여 코드를 훨씬 더 간결하고 읽기 쉽게 만들었다는 것을 깨달았습니다.

그래서인지 패턴 매칭이 정말 인상 깊었어요. Elixir의 패턴 매칭은 다음과 같습니다.

 [a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42

위의 예는 Ruby의 다중 할당 과 매우 유사합니다. 그러나 그 이상입니다. 또한 값이 일치하는지 여부도 확인합니다.

 [a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"

위의 예에서 왼쪽에 있는 숫자 42는 할당되는 변수가 아닙니다. 특정 인덱스의 동일한 요소가 오른쪽의 요소와 일치하는지 확인하는 값입니다.

 [a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value

이 예에서는 값이 할당되는 대신 MatchError 가 발생합니다. 숫자 88이 숫자 42와 일치하지 않기 때문입니다.

또한 지도(Ruby의 해시와 유사)에서도 작동합니다.

 %{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty

위의 예는 키 name 의 값이 Zote 확인하고 키 title 의 값을 변수 제목에 바인딩합니다.

이 개념은 데이터 구조가 복잡할 때 매우 잘 작동합니다. 변수를 할당하고 값이나 유형을 모두 한 줄에 확인할 수 있습니다.

또한 Elixir와 같은 동적으로 유형이 지정된 언어에서 메소드 오버로딩을 허용합니다.

 def process(%{"animal" => animal}) do IO.puts("The animal is: #{animal}") end def process(%{"plant" => plant}) do IO.puts("The plant is: #{plant}") end def process(%{"person" => person}) do IO.puts("The person is: #{person}") end

인수의 해시 키에 따라 다른 메서드가 실행됩니다.

이것이 얼마나 강력한 패턴 매칭이 가능한지 보여주길 바랍니다. noaidi, qo 및 egison-ruby와 같은 보석을 사용하여 Ruby에 패턴 일치를 가져오려는 시도가 많이 있습니다.

Ruby 2.7에도 이러한 gem과 크게 다르지 않은 자체 구현이 있으며 이것이 현재 수행되는 방식입니다.

루비 패턴 매칭 구문

Ruby에서 패턴 일치는 case 문을 통해 수행됩니다. 그러나 일반적인 when 을 사용하는 대신 in 키워드가 대신 사용됩니다. 또한 if 또는 unless 문 사용을 지원합니다.

 case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end

Case 문은 변수 또는 표현식을 허용할 수 있으며 이는 in 절에 제공된 패턴과 일치합니다. if 또는 if 문은 패턴 뒤에도 제공될 수 있습니다. 여기에서 동등성 검사는 또한 일반 case 문처럼 === 를 사용합니다. 즉, 클래스의 하위 집합과 인스턴스를 일치시킬 수 있습니다. 사용 방법의 예는 다음과 같습니다.

일치하는 배열

 translation = ['th', 'เต้', 'ja', 'テイ'] case translation in ['th', orig_text, 'en', trans_text] puts "English translation: #{orig_text} => #{trans_text}" in ['th', orig_text, 'ja', trans_text] # this will get executed puts "Japanese translation: #{orig_text} => #{trans_text}" end

위의 예에서 변수 translation 은 두 가지 패턴과 일치합니다.

['th', orig_text, 'en', trans_text]['th', orig_text, 'ja', trans_text] . 이것이 하는 일은 패턴의 값이 각 인덱스의 translation 변수 값과 일치하는지 확인하는 것입니다. 값이 일치하면 translation 변수의 값을 각 인덱스의 패턴에 있는 변수에 할당합니다.

루비 패턴 매칭 애니메이션: 배열 매칭

일치하는 해시

 translation = {orig_lang: 'th', trans_lang: 'en', orig_txt: 'เต้', trans_txt: 'tae' } case translation in {orig_lang: 'th', trans_lang: 'en', orig_txt: orig_txt, trans_txt: trans_txt} puts "#{orig_txt} => #{trans_txt}" end

위의 예에서 translation 변수는 이제 해시입니다. in 절의 다른 해시와 일치합니다. 케이스 문이 패턴의 모든 키가 translation 변수의 키와 일치하는지 확인합니다. 또한 각 키의 모든 값이 일치하는지 확인합니다. 그런 다음 해시의 변수에 값을 할당합니다.

루비 패턴 매칭 애니메이션: 배열 매칭

일치하는 하위 집합

패턴 일치에 사용되는 품질 검사는 === 논리를 따릅니다.

다중 패턴

  • | 하나의 블록에 대해 여러 패턴을 정의하는 데 사용할 수 있습니다.
 translation = ['th', 'เต้', 'ja', 'テイ'] case array in {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} | ['th', orig_text, 'ja', trans_text] puts orig_text #=> เต้ puts trans_text #=> テイend

위의 예에서 translation 변수는 {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} 해시와 ['th', orig_text, 'ja', trans_text] 해시와 일치합니다. 정렬.

이것은 동일한 것을 나타내는 약간 다른 유형의 데이터 구조가 있고 두 데이터 구조가 동일한 코드 블록을 실행하도록 하려는 경우에 유용합니다.

화살표 할당

이 경우 => 를 사용하여 일치하는 값을 변수에 할당할 수 있습니다.

 case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end

이것은 데이터 구조 내부의 값을 확인하고 이러한 값을 변수에 바인딩하려는 경우에 유용합니다.

핀 연산자

여기서 핀 연산자는 변수가 재할당되는 것을 방지합니다.

 case [1,2,2] in [a,a,a] puts a #=> 2 end

위의 예에서 패턴의 변수 a는 1, 2, 2와 일치합니다. 1, 2, 2에 할당됩니다. 배열의 값은 동일합니다.

 case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end

핀 연산자를 사용하면 변수를 재할당하는 대신 평가합니다. 위의 예에서 [1,2,2]는 [a,^a,^a]와 일치하지 않습니다. 첫 번째 인덱스에서 a가 1로 할당되기 때문입니다. 두 번째와 세 번째에서 a는 1로 평가되고, 그러나 2와 일치합니다.

그러나 [a,b,^b]는 [1,2,2]와 일치합니다. a는 첫 번째 인덱스에서 1에 할당되고 b는 두 번째 인덱스에서 2에 할당되고 이제 2인 ^b는 다음과 일치하기 때문입니다. 세 번째 인덱스에서 2이므로 통과합니다.

 a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end

case 문 외부의 변수도 위의 예와 같이 사용할 수 있습니다.

밑줄( _ ) 연산자

밑줄( _ )은 값을 무시하는 데 사용됩니다. 몇 가지 예에서 살펴보겠습니다.

 case ['this will be ignored',2] in [_,a] puts a #=> 2 end
 case ['a',2] in [_,a] => b puts a #=> 2 Puts b #=> ['a',2] end

위의 두 예에서 _ 와 일치하는 모든 값은 전달됩니다. 두 번째 case 문에서 => 연산자는 무시된 값도 캡처합니다.

Ruby의 패턴 일치 사용 사례

다음 JSON 데이터가 있다고 상상해보십시오.

 { nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }

Ruby 프로젝트에서 이 데이터를 구문 분석하고 다음 조건으로 이름을 표시하려고 합니다.

  1. 사용자 이름이 있으면 사용자 이름을 반환합니다.
  2. 닉네임, 이름, 성이 존재할 경우 닉네임, 이름, 성을 차례로 반환합니다.
  3. 닉네임이 존재하지 않지만 성과 이름이 존재하는 경우, 이름을 리턴한 다음 성을 리턴하십시오.
  4. 적용되는 조건이 없으면 "새 사용자"를 반환합니다.

지금 당장 Ruby로 이 프로그램을 작성하는 방법은 다음과 같습니다.

 def display_name(name_hash) if name_hash[:username] name_hash[:username] elsif name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last] "#{name_hash[:nickname]} #{name_hash[:realname][:first]} #{name_hash[:realname][:last]}" elsif name_hash[:first] && name_hash[:last] "#{name_hash[:first]} #{name_hash[:last]}" else 'New User' end end

이제 패턴 일치가 어떻게 보이는지 봅시다.

 def display_name(name_hash) case name_hash in {username: username} username in {nickname: nickname, realname: {first: first, last: last}} "#{nickname} #{first} #{last}" in {first: first, last: last} "#{first} #{last}" else 'New User' end end

구문 선호도는 약간 주관적일 수 있지만 저는 패턴 일치 버전을 선호합니다. 이는 패턴 일치를 통해 해시 값을 설명하고 확인하는 대신 예상하는 해시를 작성할 수 있기 때문입니다. 이렇게 하면 예상되는 데이터를 쉽게 시각화할 수 있습니다.

 `{nickname: nickname, realname: {first: first, last: last}}`

대신에:

 `name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.

Deconstruct 및 Deconstruct_keys

Ruby 2.7에는 deconstructdeconstruct_keys 라는 두 가지 새로운 특수 메서드가 도입되었습니다. 클래스의 인스턴스가 배열 또는 해시와 일치할 때 각각 deconstruct 또는 deconstruct_keys 가 호출됩니다.

이러한 방법의 결과는 패턴과 일치하는 데 사용됩니다. 다음은 예입니다.

 class Coordinate attr_accessor :x, :y def initialize(x, y) @x = x @y = y end def deconstruct [@x, @y] end def deconstruct_key {x: @x, y: @y} end end

코드는 Coordinate 라는 클래스를 정의합니다. 속성으로 x와 y가 있습니다. 또한 deconstructdeconstruct_keys 메서드가 정의되어 있습니다.

 c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end

여기에서 Coordinate 의 인스턴스가 정의되고 배열에 대해 패턴이 일치됩니다. 여기서 일어나는 일은 Coordinate#deconstruct 가 호출되고 그 결과가 패턴에 정의된 배열 [a,b] 와 일치하는 데 사용된다는 것입니다.

 case c in {x:, y:} px #=> 32 py #=> 50 end

이 예에서 동일한 Coordinate 인스턴스가 해시에 대해 패턴 일치되고 있습니다. 이 경우 Coordinate#deconstruct_keys 결과는 패턴에 정의된 해시 {x: x, y: y} 와 일치하는 데 사용됩니다.

흥미로운 실험 기능

Elixir에서 패턴 매칭을 처음 경험한 저는 이 기능이 메서드 오버로딩을 포함하고 한 줄만 필요한 구문으로 구현될 수 있다고 생각했습니다. 그러나 Ruby는 패턴 일치를 염두에 두고 구축된 언어가 아니므로 이해할 수 있습니다.

case 문을 사용하는 것은 아마도 이것을 구현하는 매우 간단한 방법일 것이며 기존 코드에도 영향을 미치지 않습니다( deconstructdeconstruct_keys 메서드 제외). case 문의 사용은 실제로 Scala의 패턴 일치 구현과 유사합니다.

개인적으로 패턴 매칭은 Ruby 개발자들에게 흥미로운 새로운 기능이라고 생각합니다. 코드를 훨씬 더 깔끔하게 만들고 Ruby를 좀 더 현대적이고 흥미롭게 만들 수 있는 잠재력이 있습니다. 나는 사람들이 이것을 어떻게 만들고 이 기능이 미래에 어떻게 발전하는지 보고 싶습니다.