Swift Predicate

Doldamul·2023년 7월 15일
0
post-thumbnail
struct Predicate<each Input> : Sendable, Codable, CodableWithConfiguration

💡 검색 또는 필터링을 목적으로 입력값들의 집합을 검사하는데 사용되는 논리적 조건식.

2023년 NSPredicate 타입 대체를 목적으로 추가되어 iOS 17, … 이상에서 사용 가능하다.

NSPredicate과 비교해 다음과 같은 장점이 있다:

  • 타입 검사를 통한 타입 안정성 보장
  • Xcode Syntax Highlighting 및 자동완성 기능 사용 가능
  • 더이상 Obj-C 구문 규칙에 구애받지 않음
  • 모든 Swift 타입에 범용적으로 사용 가능
  • Sendable 및 Codable 지원

공식 문서 및 proposal 문서, Foundation 오픈소스에서 긁어낸 내용을 다음 노션 페이지들에 정리해놓았다.

아래 글은 Predicate 노션 페이지의 내용을 조금 더 축약한 것이다.

개요

Predicate은 Boolean 값(true/false)으로 평가되는 논리적 조건식이다. 표현식(expression) 트리 구조로서 전체 predicate을 구성하고, 연산자 또한 피연산자 표현식을 포함한 하나의 표현식으로서 정의된다. 컬렉션에서 필터링을 수행하거나 특정 요소를 검색할 때 사용하는데, 구체적인 주요 활용처는 다음과 같다:

  • SwiftData
  • NSPredicate의 사용처(NSPredicate 생성자로 PredicateNSPredicate 변환 가능)
    • Spotlight
    • Core Data
    • in-memory 객체 필터링(NSArray, NSSet, NSOrderedSet, NSMutable〃)
  • Sequence 프로토콜의 filter(_ predicate:) 메소드

predicate을 작성할 때는 #Predicate 매크로를 사용하여 클로저 내에 단일 표현식으로 조건식을 작성한다:

// Predicate 클로저가 전달받는 매개변수 message는 해당 Predicate에서 평가될 값이다.
let messagePredicate = #Predicate<Message> { message in
    message.length < 100 && message.sender == "Jeremy"
}

predicate을 작성할 때 클로저를 사용하지만, #Predicate 매크로는 컴파일시 해당 클로저 코드를 PredicateExpressions 네임스페이스에 정의된 표현식(PredicateExpression) 타입 구조로 해석하여 Predicate 타입으로 변환시킨다. 따라서 클로저에 있는 코드는 프로그램에서 실제로 작동되는 것이 아니라, 매크로에 전달하는 일종의 요청일 뿐이다.

Swift Predicate에서 지원하는 표현식은 StandardPredicateExpression 프로토콜을 준수하는 표현식들만으로 한정되며, 개발자들이 해당 프로토콜을 준수하는 새 타입을 추가하는 것은 금지되어 있다.

PredicateExpressions 네임스페이스로부터, Predicate 정의 내에서 사용할 수 있는 연산 및 표현식을 알수 있다:

  • 산술 +, -, *, /%
  • 단항 부호반전 -
  • 범위 .....<
  • 비교 <<=>>===!=
  • 삼항 조건식 ? :
  • 조건 표현식 if
  • 논리 &&||!
  • Swift 옵셔널 ???!, if-let 표현식
  • 타입 캐스팅 및 타입 검사 as?as!is
  • Range 메소드 contains(_:)
  • String 메소드 caseInsensitiveCompare(_:), localizedCompare(_:), localizedStandardContains(_:)
  • Sequence 메소드 allSatisfy()filter()contains()contains(where:)starts(with:), max(), min()
  • Collection 메소드 contains(_:)
  • 서브스크립트 []
    • Collection에 index 기반 접근 subscript(position:)
    • Collection에 Range 기반 접근 subscript(bounds:)
    • Dictionary에 key값 기반 접근 subscript(key:)
    • Dictionary에 기본값 포함 key값 기반 접근 subscript(key:default:)
  • . 문법으로 멤버 접근
  • 리터럴(nil, true, false 등 포함) 및 외부 상수 사용
  • 중첩된 Predicate 사용(FoundationPreview 0.3 버전에서 추가됨)

Predicate 정의 내에서 사용할 수 없는 항목은 다음과 같다:

  • if, for문 등의 흐름 제어 '구문'(위의 조건 표현식과는 다르다.)
  • 변수값 변경 등 단일 표현식을 벗어나는 구문
  • 옵셔널 체이닝 (언래핑 후 하위 멤버 접근을 원할 경우 flatMap(_:) 메소드를 호출해서 언래핑 - 참조)
  • . 문법 또는 self 등으로 외부 변수 접근(참조) (Predicate 타입에서 가변적 타입 매개변수를 통해 외부 변수 참조 가능)

복잡한 질의 구문을 표현하기 위해 표현식 클로저를 중첩시킬 수도 있다.

// Sequence의 contains(where:) 메소드를 활용하여 중첩된 표현식 클로저를 만들었다. 
// 여전히, 해당 클로저 내부에서도 단일 표현식 형태가 요구된다.
let messagePredicate = #Predicate<Message> { message in
    message.recipients.contains {
        $0.firstName == message.sender.firstName
    }
}

가변적 타입 매개변수 문법을 활용하면 N개의 입력값을 받아 더욱 복잡한 질의 구문을 표현할 수도 있다. 평가할 값 이외에 추가 변수를 제공하고 싶은 경우 유용할 수 있다.

// 가변적 타입 매개변수 문법을 통해 클로저에서 추가 매개변수를 사용하고, 
// predicate 평가시에는 추가 인자를 전달한다.
let myPredicate = #Predicate<Message, Int> { message, limit in
    message.content.count < limit && $0.sender.firstName == "Jeremy"
}
let result = try myPredicate.evaluate(message, 280)

FoundationPreview 0.3 버전부터는 미리 정의된 predicate을 다른 predicate 내에 중첩하여, 표현식으로서 사용할 수 있게 되었다.

let ageCondition = #Predicate<Person> { age == 12 }
let heightCondition = #Predicate<Person> { height > 150 }

let predicate = #Predicate<Person> {
    ageCondition && heightCondition
}

PredicateCodable을 준수하므로 안전하게 encodingdecoding이 가능하며, 파일로부터 predicate을 불러올 수도 있다. 또한 Sendable을 준수하므로 동시성 작업 영역간 전달될 수 있다. 아카이브된 predicate을 읽어들일 때 허용할 타입 및 keypath 목록을 직접 정의하려면 PredicateCodableConfiguration을 사용해야 한다.

📎 gist.github.com: Swift Predicate Archiving Pitch - Encoded Format에서 Predicate 타입을 인코딩했을 때 생성되는 아카이브 포맷에 대해 확인할 수 있다.

expression 프로퍼티는 predicate 자료구조 전체에 해당하는 최상위 표현식을 가지므로, 해당 프로퍼티를 활용하면 predicate을 다른 표현으로 수정할 수도 있다. 예를 들어, 해당 predicate의 표현식 일부를 수정•추가하거나, 다른 질의(query) 언어 표현으로 변환할 수도 있다.

커스텀 Predicate 제작

Swift Predicate 도메인 API들은 커스텀 Predicate 타입을 제작하는 것 또한 함께 고려되었다.

커스텀 Predicate 타입을 제작할 때 고려해야할 사항들은 다음과 같다:

  • 커스텀 #Predicate 매크로를 구현해야 한다.
  • 커스텀 Predicate 타입을 구현해야 하며, 기본 Predicateexpressionvariables 프로퍼티, 생성자 및 evaluate 메소드 등과 유사한 인터페이스를 사용한다.
  • StandardPredicateExpression 프로토콜과 유사한 역할의 프로토콜을 정의하여 사용 가능한 표현식의 종류를 제한하고, 각 표현식은 PredicateExpression 프로토콜을 준수하도록 한다.
  • 입력값이 있는 경우 PredicateBinding 타입을 사용해 입력값을 predicate 자료구조에 전파한다.
  • 커스텀 Predicate 타입의 CodableWithConfiguration 프로토콜 준수를 돕는 API도 제공된다. gist.github.com: Swift Predicate Pitch - API Support for Third Party Predicates 페이지에서 해당 API 목록 및 사용 예시를 확인할 수 있다.

그러나 커스텀 Predicate 타입을 정의하는 방법 및 관련 API까지는 아직 명확한 방법 문서 또는 proposal이 올라오지 않았다. github.com/apple: Swift-Foundation 에서 Predicate 및 관련 구현 코드를 열람할 수 있으며, 8/23일 github.com/apple: Swift Foundation-macros 에 Predicate 매크로의 구현 코드가 공개되었다.

참고자료

profile
덕질은 삶의 활력소다. 내가 애플을 좋아하는 이유. 재밌거덩

0개의 댓글