• Swift project update

    • 기존에는 swift-evolution 프로세스를 통해서 언어의 추가 기능을 리뷰하고 발전시켰다.
      • https://www.swift.org/swift-evolution/ 대시보드에서 확인 가능
    • 작년에 Swift 언어와 표준 라이브러리의 발전에 대한 책임 소재를 확실히 하기 위해서 Language Steering Group을 구성했다.(관련 블로그 글)
    • 때로는 여러 proposal이 합쳐져서 광범위한 변화가 일어나기도 한다.
      • concurrency는 10개의 proposal을 통해서 도입되었다.
      • 그래서 이 proposal 들을 묶어줄 수 있는 vision이라는 문서가 추가 되었다.
    • 이는 Swift 커뮤니티가 하는 일의 일부일 뿐이다. 성공적인 언어를 위해서는 더 많은 것들이 필요하다.
      • 도구 지원
      • 다양한 플랫폼 지원
      • 풍부한 문서
    • 이를 위해서 Language Steering Group과 동시에 Ecosystem Steering Group이 추가되었다.
  • Expressivew code

    1. if-else와 switch문을 expression으로 사용 가능해졌다.

      // as-is
      let bullet =
        isRoot && (count == 0 || !willExpand) ? ""
          : count == 0    ? "- "
          : maxDepth <= 0 ? "▹ " : "▿ "
      
      // to-be
      let bullet =
          if isRoot && (count == 0 || !willExpand) { "" }
          else if count == 0 { "- " }
          else if maxDepth <= 0 { "▹ " }
          else { "▿ " }
      
    • 전역변수 등을 초기화 할 때도 클로져를 만들어서 즉석에서 실행하는 등의 테크닉을 쓸 필요가 줄었다.

      let attributedName = 
      				if let displayName, !displayName.isEmpty {
                  AttributedString(markdown: displayName)
              } else {
                  "Untitled"
              }
      
    1. resultBuilder 개선

      • 더 빠른 타입 체킹
      • 코드 자동완성 향상
      • 더 정확한 에러 메시지
      • 이 개선은 올바르지 못한 코드에 좀 더 집중함으로써 이뤄졌다.
        • 기존에는 타입 체커가 올바르지 않은 경로도 일일이 탐사하느라 검사가 오래 걸렸다.

          struct ContentView: View {
              enum Destination { case one, two }
          
              var body: some View {
                  List {
                      NavigationLink(value: .one) { //In 5.9, Errors provide a more accurate diagnostic
                          Text("one")
                      }
                      NavigationLink(value: .two) {
                          Text("two")
                      }
                  }.navigationDestination(for: Destination.self) {
                      $0.view // Error occurs here in 5.7
                  }
              }
          }
          
    2. Generic

      • 기존에 Generic인자를 여러개 쓰기 위해서는 일일이 정의해줘야 했다.

        struct Request<Result> { ... }
        
        struct RequestEvaluator {
          func evaluate<Result>(_ request: Request<Result>) -> Result
        }
        
        func evaluate(_ request: Request<Bool>) -> Bool {
          return RequestEvaluator().evaluate(request)
        }
        
        let value = RequestEvaluator().evaluate(request)
        
        let (x, y) = RequestEvaluator().evaluate(r1, r2)
        
        let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
        
        // 위 코드를 위해서는 일일이 정의
        func evaluate<Result>(_:) -> (Result)
        
        func evaluate<R1, R2>(_:_:) -> (R1, R2)
        
        func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3)
        
        func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4)
        
        func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5)
        
        func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6)
        
      • 이러면 정의되지 않은 인자 갯수가 들어오면 컴파일 에러가 난다. 이는 임의적으로 생기는 API의 한계다.

        //This will cause a compiler error "Extra argument in call"
        let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7)
        
      • 5.9에서는 제네릭 인자의 길이도 추상화가 가능해졌다. 이는 여러 타입 인자를 함께 묶어서(packed)받는 게 가능해진 덕분이다.

        <each Result>
        
      • 이제 인자별로 여러개 정의 안해도 된다.

        • 반환 값은 단일 값일 수도 있고, 여러 값이 있는 튜플일수도 있다.
        func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
        
      • 쓰는 쪽에서는 기존처럼 쓰면 된다.

        struct Request<Result> { ... }
        
        struct RequestEvaluator {
          func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
        }
        
        let results = RequestEvaluator.evaluate(r1, r2, r3)
        
    3. 매크로

      • 매크로를 통해서 보일러플레이트를 줄이면서 언어의 표현력을 높일 수 있다.

      • 예시: assert

        assert(max(a, b) == c) // max_assert.swift:5 Assertion failed
        
        XCAssertEqual(max(a, b), c) //XCTAssertEqual failed: ("10") is not equal to ("17")
        
        #assert(max(a, b) == c) // 소스코드를 읽어서 좀 더 풍부한 정보를 제공해줄 수 있다.
        

        스크린샷 2023-06-07 오전 10.39.01.png

      • Swift에서의 매크로는 타입과 함수와 동일한 API기 때문에 패키지를 통해서 배포되고 이를 정의한 모듈을 import함으로써 사용할 수 있다.

        import PowerAssert
        #assert(max(a, b) == c)
        
      • 모듈내에서 정의하는 부분을 보면 함수랑 크게 다를 게 없다.

        • 반환값이 필요하면 함수처럼 →을 사용하면 된다.

          public macro assert(_ condition: Bool)
          
        • 타입 체킹도 된다.

          import PowerAssert
          #assert(max(a, b)) //Type 'Int' cannot be a used as a boolean; test for '!= 0' instead
          
        • 매크로 정의 및 동작 원리

          • 매크로가 정의된 모듈과 해당 매크로를 정의한 타입을 string으로 명시한다.

            public macro assert(_ condition: Bool) = #externalMacro(
              module: “PowerAssertPlugin”,
              type: “PowerAssertMacro"
            )
            
          • 매크로는 별도 프로그램으로 정의되어 컴파일러의 플러그인으로 들어간다.

            • 컴파일러가 소스코드를 넘기면, 이 프로그램이 새로운 코드를 반환해서 컴파일러가 이를 통합하는 방식

              // input
              #assert(a == b)
              
              // output
              
              PowerAssert.Assertion(
                "#assert(a == b)"
              ) {
                $0.capture(a, column: 8) == $0.capture(b, column: 13)
              }
              
          • 매크로 선언에는 해당 매크로의 역할도 명시한다.

            • ex. assert는 freestanding(expression)이다. 이는 독립적으로 expression으로 쓰인다는 뜻이다.

              // Freestanding macro roles
              
              @freestanding(expression)
              public macro assert(_ condition: Bool) = #externalMacro(
                module: “PowerAssertPlugin”,
                type: “PowerAssertMacro"
              )
              
            • ex. Foundation의 새로운 Predicate API

              let pred = #Predicate<Person> {
                $0.favoriteColor == .blue
              }
              
              let blueLovers = people.filter(pred)
              
              // Predicate expression macro
              
              @freestanding(expression) 
              public macro Predicate<each Input>(
                _ body: (repeat each Input) -> Bool
              ) -> Predicate<repeat each Input>
              
            • ex. caseDetection. enum에서 특정 케이스인지를 확인하는 기능

              • 이는 attached 매크로다.
              // as-is
              enum Path {
                case relative(String)
                case absolute(String)
              }
              
              let absPaths = paths.filter { $0.isAbsolute }
              
              extension Path {
                var isAbsolute: Bool {
                  if case .absolute = self { true }
                  else { false }
                }
              }
              
              extension Path {
                var isRelative: Bool {
                  if case .relative = self { true }
                  else { false }
                }
              }
              
              // to-be
              @CaseDetection
              enum Path {
                case relative(String)
                case absolute(String)
              }
              
              let absPaths = paths.filter { $0.isAbsolute }
              
              // 위 매크로는 다음코드로 expantion 된다.
              enum Path {
                case relative(String)
                case absolute(String)
                
                //Expanded @CaseDetection macro integrated into the program.
                var isAbsolute: Bool {
                  if case .absolute = self { true }
                  else { false }
                }
              
                var isRelative: Bool {
                  if case .relative = self { true }
                  else { false }
                }
              }
              
            • attached macro의 역할

              • member: 타입이나 extension에 새로운 멤버를 추가한다.
              • peer: 해당 선언에 맞는 추가적인 선언을 추가해준다.(ex. async함수의 completion handler버전, 혹은 반대)
              • accessor: stored property를 computed property로 바꿔서 접근 과정에서의 추가 로직 실행
                • property wrapper와 비슷하지면 좀 더 유연하다
              • memberAttribute: 특정 멤버나 타입에 속성 추가
              • comformance: 프로토콜 채택 추가

              스크린샷 2023-06-07 오전 11.08.10.png

            • attached macro는 여러개가 합쳐질 수 있다.

              • ex. observation

                // Observation in SwiftUI(5.9 이전)
                
                final class Person: ObservableObject {
                    @Published var name: String
                    @Published var age: Int
                    @Published var isFavorite: Bool
                }
                
                struct ContentView: View {
                    @ObservedObject var person: Person
                    
                    var body: some View {
                        Text("Hello, \\(person.name)")
                    }
                }
                
                // Observation in SwiftUI(5.9 이후)
                
                @Observable final class Person {
                    var name: String
                    var age: Int
                    var isFavorite: Bool
                }
                
                struct ContentView: View {
                    var person: Person
                    
                    var body: some View {
                        Text("Hello, \\(person.name)")
                    }
                }
                
              • Obsrevation 매크로 정의

                
                // Observation 매크로 정의
                @attached(member, names: ...)
                @attached(memberAttribute)
                @attached(conformance)
                public macro Observable() = #externalMacro(...).
                
            • 매크로는 순서대로 펼쳐진다.

              • 안 펼쳐진 상태

                @Observable final class Person {
                    var name: String
                    var age: Int
                    var isFavorite: Bool
                }
                
              • member 매크로를 펼친 상태

                @Observable final class Person {
                    var name: String
                    var age: Int
                    var isFavorite: Bool
                  
                		internal let _$observationRegistrar = ObservationRegistrar<Person>()
                    internal func access<Member>(
                        keyPath: KeyPath<Person, Member>
                    ) {
                        _$observationRegistrar.access(self, keyPath: keyPath)
                    }
                    internal func withMutation<Member, T>(
                        keyPath: KeyPath<Person, Member>,
                        _ mutation: () throws -> T
                    ) rethrows -> T {
                        try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
                    }
                }
                
              • memberAttribute 매크로 펼친 상태

                @Observable final class Person {
                    @ObservationTracked var name: String
                    @ObservationTracked var age: Int
                    @ObservationTracked var isFavorite: Bool
                  
                		internal let _$observationRegistrar = ObservationRegistrar<Person>()
                    internal func access<Member>(
                        keyPath: KeyPath<Person, Member>
                    ) {
                        _$observationRegistrar.access(self, keyPath: keyPath)
                    }
                    internal func withMutation<Member, T>(
                        keyPath: KeyPath<Person, Member>,
                        _ mutation: () throws -> T
                    ) rethrows -> T {
                        try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
                    }
                
              • ObservationTracked 매크로에 의해서 accessor가 추가된 형태

                @Observable final class Person {
                    @ObservationTracked var name: String { get { … } set { … } }
                    @ObservationTracked var age: Int { get { … } set { … } }
                    @ObservationTracked var isFavorite: Bool { get { … } set { … } }
                  
                		internal let _$observationRegistrar = ObservationRegistrar<Person>()
                    internal func access<Member>(
                        keyPath: KeyPath<Person, Member>
                    ) {
                        _$observationRegistrar.access(self, keyPath: keyPath)
                    }
                    internal func withMutation<Member, T>(
                        keyPath: KeyPath<Person, Member>,
                        _ mutation: () throws -> T
                    ) rethrows -> T {
                        try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
                    }
                }
                
              • comformance 매크로에 의해 Observable 프로토콜 채택 추가

                @Observable final class Person: Observable {
                    @ObservationTracked var name: String { get { … } set { … } }
                    @ObservationTracked var age: Int { get { … } set { … } }
                    @ObservationTracked var isFavorite: Bool { get { … } set { … } }
                  
                		internal let _$observationRegistrar = ObservationRegistrar<Person>()
                    internal func access<Member>(
                        keyPath: KeyPath<Person, Member>
                    ) {
                        _$observationRegistrar.access(self, keyPath: keyPath)
                    }
                    internal func withMutation<Member, T>(
                        keyPath: KeyPath<Person, Member>,
                        _ mutation: () throws -> T
                    ) rethrows -> T {
                        try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
                    }
                }
                
            • 이렇게 만들어진 코드는 일반적인 Swift코드다.

            • Xcode에서는 매크로를 자동으로 펼쳐줄 수 있는 기능을 제공한다.

  • Swfit everywhere

  • Wrapup - foundationDB