애플사이다의 iOS 개발 일지

[Swift] API Design Guidelines - 이름 짓기의 기준 본문

Swift/영문 공식문서 뜯어보기-Swift

[Swift] API Design Guidelines - 이름 짓기의 기준

Applecider 2021. 10. 17. 05:19

안녕하세요. 애플사이다 입니다.

변수 이름, 함수 이름 짓기는 모든 개발자의 영원한 숙제입니다.

어떻게 하면 코드를 10년 뒤에 다시 봐도 3초 만에 이해할 수 있도록 네이밍할 수 있을까요?

 

Swift에서는 API Design Guidelines라는 공식문서를 통해 변수/함수/매개변수/전달인자 레이블의 네이밍 및 주석 작성 방법에 대한 기준을 제시합니다.

Swift Language Guide 만큼이나 중요한 문서입니다.

 


API Design Guidelines

*API : Application Programming Interface

 

Fundamentals (기본 원칙)

1. 사용 시점을 기준으로 명확히 작성하는 것이 가장 중요한 목표이다. (Clarity at the point of use is your most important goal.) 메서드 및 프로퍼티와 같은 Entities는 한 번 선언하면 반복적으로 사용한다. 이러한 Entities를 명확하고 간결하게 사용하도록 API를 설계해야 한다. 디자인을 검토할 때 선언부를 읽는 것만으로는 부족하다. 항상 사용 시점의 상황을 검토하여 문맥상 명확한지 확인해야 한다. 

(Entities such as methods and properties are declared only once but used repeatedly. Design APIs to make those uses clear and concise. When evaluating a design, reading a declaration is seldom sufficient; always examine a use case to make sure it looks clear in context.)

*Entity : 저장하고, 관리할 데이터의 집합이다.

 

2. ❗️명확성이 간결성보다 중요하다. (Clarity is more important than brevity.) 단순히 글자 수가 적은 코드를 만드는 게 목표가 아니다. Swift 코드의 간결성은 강력한 type 시스템과 반복적으로 재사용하는 코드를 줄이는 기능으로 인해 나타난 부수적인 효과일 뿐이다. 

(Although Swift code can be compact, it is a non-goal to enable the smallest possible code with the fewest characters. Brevity in Swift code, where it occurs, is a side-effect of the strong type system and features that naturally reduce boilerplate.) *boilerplate : 표준 형식. boilerplate는 반복적으로 재사용하는 코드를 의미한다.

 

3. 모든 선언에 대해 문서화 주석을 작성한다. 문서 작성을 통해 얻는 통찰력은 디자인에 큰 영향을 미칠 수 있다. 

(Write a documentation comment for every declaration. Insights gained by writing documentation can have a profound impact on your design, so don’t put it off.)
✅ 단, 주석 사용을 최소화해야 한다고 주장하는 개발자도 있다.

Note: API의 기능을 간단히 설명할 수 없으면, API를 잘못 설계했을 가능성이 높다.

  • Xcode 자동완성에서 확인 가능하도록 Swift Markdown을 사용한다.
  • ✅ 가장 먼저, 요약을 통해 선언한 Entity에 대해 설명한다. API 자체는 선언부와 요약만으로도 완벽히 이해할 수 있다.
    /// Returns a "view" of `self` containing the same elements in reverse order.
    func reversed() -> ReverseCollection​
    • 가능하면 1개의 문구 (a single sentence fragment)로 작성하고, 마침표로 끝낸다. 완전한 문장 (complete sentence)을 사용하지 않는다.
    • 함수의 기능과 반환값을 설명한다. null 효과 및 Void 반환은 설명을 생략한다.
      /// Inserts `newHead` at the beginning of `self`.
      mutating func prepend(_ newHead: Int)
      
      /// Returns a `List` containing `head` followed by the elements of `self`.
      func prepending(_ head: Element) -> List
      
      /// Removes and returns the first element of `self` if non-empty; returns `nil` otherwise.
      mutating func popFirst() -> Element?
      // Note: 드물게 popFirst 함수와 같이 요약문구가 여러 문장인 경우, 문장을 ;으로 구분한다.​
    • Subscript가 접근하는 대상을 설명한다.
    • 이니셜라이저가 생성하는 대상을 설명한다.
    • 그 외의 경우, 선언된 Entity(프로퍼티 및 메서드 등)가 무엇인지 설명한다.
      /// Accesses the `index`th element.
      subscript(index: Int) -> Element { get set }
      
      /// Creates an instance containing `n` repetitions of `x`.
      init(count n: Int, repeatedElement x: Element)
      
      /// A collection that supports equally efficient insertion/removal at any position.
      struct List {
          /// The element at the beginning of `self`, or `nil` if self is empty.
          var first: Element?
          // ...
      }
  • 필요 시 1개 이상의 문단 (paragraphs)과 글머리 기호 (bullet items)를 추가한다. 문단은 한 줄을 띄어 쓰고 (separated by blank), 완전한 문장 (complete sentences)을 사용한다.
    /// Writes the textual representation of each    ← Summary
    /// element of `items` to the standard output. 
    /// (각 'items' element의 텍스트 표현을 표준 출력에다 적는다.) 
    ///                                              ← Blank line
    /// The textual representation for each item `x` ← Additional discussion (추가 설명)
    /// is generated by the expression `String(x)`.
    ///
    /// - Parameter separator: text to be printed    ⎫
    ///   between items.                             ⎟
    /// - Parameter terminator: text to be printed   ⎬ Parameters section
    ///   at the end.                                ⎟
    ///                                              ⎭
    /// - Note: To print without a trailing          ⎫
    ///   newline, pass `terminator: ""`             ⎟
    ///                                              ⎬ Symbol commands (기타 참고사항)
    /// - SeeAlso: `CustomDebugStringConvertible`,   ⎟
    ///   `CustomStringConvertible`, `debugPrint`.   ⎭
    public func print(
      _ items: Any..., separator: String = " ", terminator: String = "\n") // 가변 매개변수니까 argument items는 array 형태로 전달된다.​
    • 요약 외의 추가 정보는 symbol documentation markup을 사용하여 제공한다.
    • symbol command syntax를 사용한다. Xcode는 아래 키워드로 시작하는 글머리 기호 (bullet items)를 취급하는 약속된 방식이 있다.
      (Attention Author Authors Bug Complexity Copyright Date Experiment Important Invariant Note Parameter Parameters Postcondition Precondition Remark Requires Returns SeeAlso Since Throws ToDo Version Warning)

Naming (이름 짓기)

Promote Clear Usage (이름으로 명확한 사용법을 제시하기)

  • 코드를 읽는 사람이 이해하는 데 필요한 모든 단어를 사용한다.
    ex. Collection 내부의 주어진 위치에 있는 element를 제거하는 메서드를 고려한다면,
    extension List {
      public mutating func remove(at position: Index) -> Element
    }
    employees.remove(at: x) - 좋은 예시. x가 삭제할 element의 위치를 나타내는 것이 명확하다.
    employees.remove(x)     - 나쁜 예시. x라는 element를 제거한다는 의미로 오인할 수 있다.
  • 불필요한 단어는 생략한다. 모든 단어는 사용 시점에서 핵심 정보를 전달해야 한다. ✅ 단순히 type 정보를 나타내는 단어는 생략한다.
    public mutating func remove(member: Element) -> Element? 
    allViews.remove(cancelButton) - 좋은 예시. 명확하다.
    
    public mutating func removeElement(member: Element) -> Element?
    allViews.removeElement(cancelButton) - 나쁜 예시. 함수 호출 시 Element라는 단어는 핵심 정보가 아니다.​
  • ✅ 변수/매개변수/연관 type의 이름은 type 제약 조건이 아니라 "역할"에 따라 지정한다.
    var greeting = "Hello" - 좋은 예시. Entity의 역할을 표현하는 이름을 사용한다.
    protocol ViewController {
    	  associatedtype ContentView : View
    }
    class ProductionLine {
    	  func restock(from supplier: WidgetFactory)
    }
    
    var string = "Hello" - 나쁜 예시. type 이름을 중복해서 나타낸다.
    protocol ViewController {
    	  associatedtype ViewType : View
    }
    class ProductionLine {
    	  func restock(from widgetFactory: WidgetFactory)
    }
    • 단, 연관 타입 (associated type)이 프로토콜의 제약사항과 밀접한 연관이 있어서 프로토콜 이름이 해당 역할 자체인 경우, 프로토콜 이름에 "Protocol"을 덧붙여서 충돌을 방지한다.
      protocol Sequence {
        associatedtype Iterator : IteratorProtocol
      }
      protocol IteratorProtocol { ... }
  • 매개변수의 역할을 명시하기 위해 부족한 type 정보를 보완한다. (Compensate for weak type information to clarify a parameter’s role.) 매개변수 타입이 NSObject, Any, AnyObject, 기본 타입 (Int, String 등)인 경우, 사용 위치에서 타입 정보가 불명확하여 해당 문맥만으로는 의도를 파악하기 어려울 수 있다.
    func addObserver(_ observer: NSObject, forKeyPath path: String) - 좋은 예시. weak type parameter인 path의 역할을 설명하는 명사(forKeyPath)를 붙여서 정보를 보완했다.
    grid.addObserver(self, forKeyPath: graphics) // clear
    
    func add(_ observer: NSObject, for keyPath: String) - 나쁜 예시. 선언은 명확하지만, 사용 시점 (use site)에서 모호하다.
    grid.add(self, for: graphics) // vague
     

❗️Strive for Fluent Usage (쉽고 명확하게 읽히도록 작성하기)

  • ❗️함수를 사용하는 위치에서 함수 이름이 '영어 문장의 형태'가 되도록 한다. (Prefer function names that make use sites form grammatical English phrases.)
    x.insert(y, at: z)          “x, insert y at z” (x에 y를 z위치에 삽입)  // 좋은 예시
    x.subViews(havingColor: y)  “x's subviews having color y” (x의 subviews는 색상 y을 가짐)
    x.capitalizingNouns()       “x, capitalizing nouns” (x의 명사를 대문자화)
    
    x.insert(y, position: z)  // 나쁜 예시
    x.subViews(color: y)
    x.nounCapitalize()​
  • ✅ 첫 번째 또는 두 번째 argument 다음에 전달하는 값들이 중요하지 않으면, 가독성을 위해 해당 값 앞에서 줄바꿈을 한다.
    AudioUnit.instantiate(
      with: description, 
      options: [.inProcess], completionHandler: stopProgressBar) - 중요도가 낮은 arguments이므로 의도적으로 줄바꿈 한다.​
  • factory method의 이름은 make로 시작한다. ex. x.makeIterator()
  • 앞서 함수 사용 시점에서 함수 이름이 '영어 문장의 형태'가 되어야 한다고 했다. 하지만 그렇다고 해서 (이니셜라이저 및 factory method 호출 시) ✅ 첫 번째 argument를 포함하여 영어 문장을 구성해서는 안된다. ex. x.makeWidget(cogCount: 47)
    (The first argument to initializer and factory methods calls should not form a phrase starting with the base name.)
    let foreground = Color(red: 32, green: 64, blue: 128) - 좋은 예시. 이니셜라이저 및 factory method 호출 시, 첫번째 argument를 문장의 일부로 만들지 않았다. 
    let newPart = factory.makeWidget(gears: 42, spindles: 14)
    
    let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128) - 나쁜 예시. 첫번째 argument에 추가 설명을 덧붙여 문장을 만들었다.
    let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)​
  • 함수 이름은 Side-effects 여부에 따라 지정한다.
    *Side-effects : 특정 작업의 결과로 의도하지 않은 부수적인 효과가 발생 가능하다는 의미이다.
    • side-effects가 없는 경우 명사구로 지정한다. ex. x.distance(to: y)i.successor()
    • side-effects가 있는 경우 명령형 동사구 (imperative verb phrases)로 지정한다. ex. print(x)x.sort()x.append(y)
    • mutating (가변, ↔ immutating) 및 nonmutating (의도적으로 변경하지 않는) 메서드 pair의 이름은 일관성 있게 지정한다.
      // 참고 - mutating 키워드 : 구조체의 메서드가 구조체 내부에서 데이터 수정 할떄는 Mutating 키워드를 선언 해주어야 함
      struct Point {
      var x = 0 
      var y = 0 
      
      mutating func moveTo(x: Int, y: Int) {
      	self.x = x  // mutating 키워드가 없으면 컴파일 에러 발생
      	self.y = y 
        }
      }
      • 메서드의 동작을 동사로 설명하는 것이 자연스러울 때, *mutating 메서드의 이름으로 명령형 동사를 사용한다. *nonmutating 메서드의 이름에 "ed" 또는 "ing" suffix를 붙인다.
        - mutating 메서드 :  x.sort(), x.append(y)
        - nonmutating 메서드 :  z = x.sorted(), z = x.appending(y)
      • 동사의 과거분사 (보통 "ed"를 붙임)를 사용하여 nonmutating 메서드의 이름을 지정한다.
        /// Reverses `self` in-place. (뒤집음) <- 비교용
        mutating func reverse()
        
        /// Returns a reversed copy of `self`. (뒤집힌 상태의 복사본을 반환함)
        func reversed() -> Self
        ...
        x.reverse()
        let y = x.reversed()
      • 동사가 목적어를 가지므로 "ed"를 붙이는 것이 문법에 맞지 않을 경우, 동사의 현재분사 (보통 "ing"를 붙임)를 사용하여 nonmutating 메서드의 이름을 지정한다.
        /// Strips all the newlines from `self` (줄바꿈을 제거함) <- 비교용
        mutating func stripNewlines()
        
        /// Returns a copy of `self` with all the newlines stripped. (줄바꿈을 제거한 상태의 복사본을 반환함)
        func strippingNewlines() -> String
        ...
        s.stripNewlines()
        let oneLine = t.strippingNewlines()
      • 메서드의 동작을 명사로 설명하는 것이 자연스러울 때, *nonmutating메서드의 이름으로 명사를 사용하고, *mutating 메서드의 이름에 "form" prefix를 붙인다.
        - nonmutating 메서드 : x = y.union(z), j = c.successor(i)
        - mutating 메서드 : y.formUnion(z), c.formSuccessor(&i)
  • ✅ Bool 타입의 nonmutating 메서드/프로퍼티는 receiver (반환값)에 대한 주장 (Assertion) 형태로 이름을 지정한다. ex. x.isEmpty, line1.intersects(line2)
  • ✅ 어떤 것인지를 설명 (describe what something is)하는 Protocol은 명사로 이름을 지정한다. ex. Collection
  • ✅ 기능 (capability)을 설명하는 Protocol은 "able, ible, ing" suffix를 붙인다. ex. Equatable, ProgressReporting
  • 이외 타입, 프로퍼티, 상수/변수는 명사로 이름 짓는다.

Use Terminology Well (용어를 제대로 사용하기)

*Term of Art : 특정 분야에서 통용되는 전문적인 용어를 의미한다.

  • 일반적인 단어가 의미를 잘 전달할 수 있다면 모호한 용어는 피한다. “피부(skin)”가 목적에 맞는 용어라면 “표피(epidermis)"를 사용하지 않는다. Terms of art는 중요한 커뮤니케이션 수단이지만, 의미를 엄밀히 구분할 필요가 있을 때만 사용한다.
  • Term of Art를 사용한다면 기존에 사용하는 관습적인 의미에 충실해야 한다.
    • 전문가를 놀라게 하지 않는다 : 기존 용어를 다른 의미로 사용하면, 이미 해당 용어에 익숙한 사람이 당황할 수 있다.
    • 초보자를 혼란스럽게 하지 않는다 : 초보자는 웹 검색을 통해 기존에 통용되는 의미를 받아들인다.
  • 약어 (abbreviations)를 피한다. 특히 표준이 아닌 약어는 풀어쓰는 과정에서 오인될 수 있다. (Abbreviations, especially non-standard ones, are effectively terms-of-art, because understanding depends on correctly translating them into their non-abbreviated forms.) 약어를 사용한다면 웹 검색으로 쉽게 찾을 수 있어야 한다.
  • 선례 (precedent)를 따른다. 초보자를 위해 용어를 최적화하지 않는다.
    • 예시-1. 초보자는 List를 더 쉽게 이해할 수 있을지라도 Array를 사용하는 게 좋다. Array는 모든 프로그래머가 익숙한 기초 용어이기 때문이다.
    • 예시-2. 수학 등 특정 프로그래밍 Domain에서도 프로그래머/수학자에게 익숙한 용어인 "sin(x)"을 사용하는 게 좋다. 원칙적으로는 "sine"이 complete word 임에도 불구하고, 약어가 더 많이 통용되기 때문이다. (In this case, precedent outweighs the guideline to avoid abbreviations.)

Conventions (컨벤션)

General Conventions (일반적인 규칙)

  • complexity가 O(1)이 아닌 연산 프로퍼티는 모두 문서화하여 설명한다. (Document the complexity of any computed property that is not O(1).) 종종 프로퍼티 접근이 중요한 연산을 거치지 않는다고 생각한다. 이는 사람들이 저장 프로퍼티를 mental model로 여기기 때문이다. 하지만 그렇지 않을 때가 있다.
    *mental model : 어떤 사물을 기억하기 위해 중요한 특징이라고 인식하는 것이다. 여기서는 프로퍼티를 기억할 때 보통 저장 프로퍼티를 먼저 떠올린다는 의미이다.
  • free function 보다 메서드 및 프로퍼티가 선호된다. free function은 특수한 상황에서만 사용한다.
    1. 명확히 self가 없을 때 
    2. 함수가 제약 없는 제네릭 (unconstrained generic)일 때
    3. 함수 문법이 기존 도메인 표기법을 따를 때 
    min(x, y, z) // 1
    print(x)     // 2
    sin(x)       // 3​
  • 대소문자 표기법 (Case conventions)을 따른다. Type 및 Protocol의 이름은 UpperCamelCase를 사용하고, 그 외는 모두 lowerCamelCase를 사용한다. 또한 미국식 영어에서 대문자로 표현하는 Acronyms (두문자어, 머리글자만 따서 만든 단어) 및 Initialisms (이니셜)은 일관되게 대문자 또는 소문자로 사용한다. 그외 Acronyms은 일반적인 단어로 취급한다.
    var utf8Bytes: [UTF8.CodeUnit]        - UTF8
    var isRepresentableAsASCII = true     - ASCII
    var userSMTPServer: SecureSMTPServer  - SMTP
    
    var radarDetector: RadarScanner       - Radar
    var enjoysScubaDiving = true          - Scuba
  • 동일한 기본 의미를 갖고 있거나, 특정 domain에서만 동작할 때 메서드는 base name을 공유할 수 있다.
    *iff : (수학 용어) if and only if (필요충분조건을 나타낸다.)
    메서드가 본질적으로 동일하게 동작하므로 base name을 공유한다. (parameter type은 다르지만)
    extension Shape {  
      /// Returns `true` iff `other` is within the area of `self`.
      func contains(_ other: Point) -> Bool { ... }
    
      /// Returns `true` iff `other` is entirely within the area of `self`.
      func contains(_ other: Shape) -> Bool { ... }
    
      /// Returns `true` iff `other` is within the area of `self`.
      func contains(_ other: LineSegment) -> Bool { ... }
    }
    • geometric types 및 collections은 별도의 domain 이므로 동일한 프로그램 내에서 메서드 이름이 동일해도 된다.
      extension Collection where Element : Equatable {
        /// Returns `true` iff `self` contains an element equal to `sought`.
        func contains(_ sought: Element) -> Bool { ... }
      }​
    • 메서드의 기능이 다르면 서로 다른 이름으로 지정해야 한다.
      extension Database {  - 나쁜 예시. 메서드의 의미 및 동작이 다르므로 다른 이름을 지정해야 한다.
        /// Rebuilds the database's search index
        func index() { ... }
      
        /// Returns the `n`th row in the given table.
        func index(_ n: Int, inTable: TableID) -> TableRow { ... }
      }​
    • 타입 추론 (type inference) 기능으로 인한 모호성이 발생하므로 “overloading on return type”을 피한다.
      ✅ 즉, 반환 타입이 다른 경우 문법적으로는 overloading이 가능하지만, 함수 이름을 다르게 지정하는 것이 바람직하다.
      extension Box {  - 나쁜 예시. return type이 다르므로 다른 이름을 지정하는게 좋다.
        /// Returns the `Int` stored in `self`, if any, and `nil` otherwise.
        func value() -> Int? { ... }
      
        /// Returns the `String` stored in `self`, if any, and `nil` otherwise.
        func value() -> String? { ... }
      }​

Parameters (매개변수 컨벤션)

func move(from start: Point, to end: Point)
  • ❗️문서의 가독성을 높이는 매개변수 이름을 지정한다. 함수 사용 시점에 매개변수 이름이 보이지 않을 수 있지만, 함수를 설명하는 중요한 역할을 한다.
    /// Return an `Array` containing the elements of `self` that satisfy `predicate`.  - 좋은 예시. 자연스럽게 읽힌다.
    func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
    
    /// Replace the given `subRange` of elements with `newElements`.
    mutating func replaceRange(_ subRange: Range, with newElements: [E])
    
    
    /// Return an `Array` containing the elements of `self` that satisfy `includedInResult`.  - 나쁜 예시. 어색하고 문법에 맞지 않는다.
    func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]
    
    /// Replace the range of elements indicated by `r` with the contents of `with`.
    mutating func replaceRange(_ r: Range, with: [E])
  • ❗️매개변수 기본값 (defaulted parameter)을 사용하여 함수 사용을 단순화한다. 매개변수가 1개이고, 전달하는 값이 대부분 정해져 있는 경우 (parameter with a single commonly-used value) 매개변수 기본값을 지정하는 것이 좋다.
    let order = lastName.compare(royalFamilyName) - 좋은 예시. 함수 사용이 간단하다.
    
    let order = lastName.compare( - 나쁜 예시. 관련 없는 정보를 숨기지 않아서 가독성이 나쁘다.
      royalFamilyName, options: [], range: nil, locale: nil)​
    • 매개변수 기본값은 보통 method family (메서드 집합) 보다 선호되는데, API를 읽을 때 부담이 적기 때문이다.
      사용자 입장에서는 method family의 모든 member에 대한 개별적인 설명을 읽고 이해해야만 메서드를 사용할 수 있다. 
      method family 중에서 사용할 메서드를 결정하려면, 모든 메서드를 이해해야 하기 때문이다. 또한 가끔 예상치 못한 상황에서 (예를 들어 foo(bar: nil) 및 foo()는 항상 동의어가 아니다.) 미세한 차이를 찾기 위해 전체 문서를 확인해야 하는 불편함을 초래한다. 따라서 매개변수 기본값을 사용하는 단일 메서드를 사용하면 보다 나은 개발이 가능하다.
      extension String {  // 좋은 예시. 읽을 때 부담이 적다.
        /// ...description...
        public func compare(
           _ other: String, options: CompareOptions = [],
           range: Range? = nil, locale: Locale? = nil
        ) -> Ordering
      }
      
      extension String {  // 나쁜 예시. method family는 읽기 복잡하다. (다른 개수의 parameter로 overload)
        /// ...description 1...
        public func compare(_ other: String) -> Ordering
        /// ...description 2...
        public func compare(_ other: String, options: CompareOptions) -> Ordering
        /// ...description 3...
        public func compare(_ other: String, options: CompareOptions, range: Range) -> Ordering
        /// ...description 4...
        public func compare(_ other: String, options: StringCompareOptions, range: Range, locale: Locale) -> Ordering
      }
  • ❗️매개변수 기본값이 있는 parameter는 parameter list의 끝부분에 배치한다. 매개변수 기본값이 없는 parameter가 메서드의 의미상 더 중요하고, 메서드 호출 위치에서 안정적인 초기화 형태를 제공하기 때문이다.

Argument Labels (전달인자 레이블 컨벤션)

*참고 - 함수 선언 시, "전달인자 레이블을 따로 정의하지 않으면, default로 "매개변수 이름"을 전달인자 레이블로 사용한다.

func move(from start: Point, to end: Point)
x.move(from: x, to: y)
  • 전달인자 레이블이 전달인자를 구분하는데 유용하지 않으면, 전달인자 레이블을 생략한다.
    ex. min(number1, number2), zip(sequence1, sequence2) 
  • 이니셜라이저가 value 보존 type 변환 (value preserving type conversions)을 할 때, 첫 번째 전달인자 레이블을 생략한다.
    ex. Int64(someUInt32)
    extension String { - 좋은 예시. type 변환 시, 첫번째 전달인자는 항상 변환 대상 (source)이다.
      // Convert `x` into its textual representation in the given radix. (*radix : 진법)
      init(_ x: BigInt, radix: Int = 10)   ← Note the initial underscore 
    }
    
    text = "The value is: "
    text += String(veryLargeNumber)
    text += " and in hexadecimal, it's"
    text += String(veryLargeNumber, radix: 16)
    • ✅ 단, “narrowing” 타입 변환 시, narrowing을 설명하는 전달인자 레이블을 사용한다. 
      *정밀한 (narrowing) 타입 변환은 레이블에 정밀도를 표현한다. (아래 코드에서 64비트에서 32비트로 단순히 줄어들 때는 truncating을, 64비트에서 32비트 근사값을 처리할 때는 saturating으로 표기하고 있다.)
      extension UInt32 {
        /// Creates an instance having the specified `value`.
        init(_ value: Int16)            ← Widening, so no label
        /// Creates an instance having the lowest 32 bits of `source`.
        init(truncating source: UInt64)
        /// Creates an instance having the nearest representable approximation of `valueToApproximate`. (표현 가능한 가장 가까운 근사치) *approximation : 근사치
        init(saturating valueToApproximate: UInt64)
      }
       
      • 값 보존 type 변환 (value preserving type conversion)은 *monomorphism (단형성)이다. 즉, 변환 대상 (source)인 값의 차이는 결과값의 차이를 초래한다. 예를 들어, Int8에서 Int64로의 type 변환은 값을 보존한다. 반면, 반대 방향으로의 type 변환은 값 보존이 불가하다. Int64의 값은 Int8로 표현 가능한 값보다 더 많기 때문이다.
      • Note: 원본 값을 얻는 수 있는지 (the ability to retrieve the original value)는 해당 type 변환이 값을 보존하는지 여부와 관련이 없다. (값을 보존하지 않더라도 원래 값을 그대로 가져올 수 있는 경우도 있고 값에 따라 다를 수 있다.)

        *참고 - (OOP 용어) 다형성 (polymorphism)이란 프로그램 언어의 각 요소들 (상수, 변수, 객체, 함수 등)이 다양한 데이터 타입에 속하는 것이 허용되는 성질이다. 
        - 단형성 : 함수는 고유의 이름으로 식별되며, 한 가지 의미를 가진다. 따라서 다른 기능을 구현하려면 다른 이름을 사용해야 한다. 
        - 다형성 : 다형성 체계에서는 범용 메서드를 선언할 때, 형태에 따라 적절한 변환 방식을 정의했기 때문에 객체의 종류와 상관없이 추상화를 높일 수 있다.
        string = StringFromNumber(number); // 단형성 (Java)
        string = stringFromDate(date);
        
        string = number.stringValue();    // 다형성 
        string = date.StringValue();
         
  • ✅ 첫 번째 전달인자가 전치사구 (prepositional phrase)의 일부인 경우, 전달인자 레이블을 사용한다. 이때 전달인자 레이블은 보통 전치사로 시작한다. ex. x.removeBoxes(havingLength: 12)
    단, 첫 번째 및 두번째 전달인자가 동일한 추상화 수준인 경우 (when the first two arguments represent parts of a single abstraction), 예외적으로 전달인자 레이블 앞에 전치사를 적는다.
    a.moveTo(x: b, y: c)  - 좋은 예시. 예외 상황에 해당하므로 전달인자 레이블 앞에 전치사를 적는다.
    a.fadeFrom(red: b, green: c, blue: d)
    
    a.move(toX: b, y: c)  - 나쁜 예시
    a.fade(fromRed: b, green: c, blue: d)

  • 그와 달리, 첫번째 전달인자가 문법에 맞는 문장의 일부인 경우, 전달인자 레이블을 생략하고, base name (기본 함수 이름)에 선행 단어 (preceding word)를 붙인다. ex. x.addSubview(y)
    ❗️ 즉, 첫번째 전달인자가 문법에 맞는 문장의 일부가 아닐 경우에는 전달인자 레이블을 사용해야 한다는 뜻이다.
    view.dismiss(animated: false)  - 좋은 예시. 전달인자 레이블을 사용하여 문법에 맞는 문장을 만들었다.
    let text = words.split(maxSplits: 12)
    let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
    x.addSubView(y)  - 전달인자 레이블 없이도 문법에 맞으며, 선행 단어 (SubView)를 붙여서 의미를 명확히 했다.
    
    view.dismiss(false)  - Don't dismiss? Dismiss a Bool?  - 나쁜 예시. 문법에는 맞지만, 잘못된 의미로 오인될 수 있다.
    words.split(12)      - Split the number 12?
    Note: 매개변수 기본값이 있는 매개변수는 함수 호출 시 생략 가능하며, 이 경우 해당 매개변수를 문장에 포함시키지 않는다.
    만약 포함시키면 해당 전달인자 레이블을 항상 사용해야 하기 때문이다.
  • 그 외의 경우, 모든 전달인자에 전달인자 레이블을 지정한다.

Special Instructions (특별 지침)

  • API에서 나타나는 위치에 1) Tuple member의 전달인자 레이블을 지정하고, 2) 클로저 매개변수의 이름을 지정한다. 이렇게 지정된 이름은 설명 기능을 가지며, 주석에서 참고할 수 있고, tuple member에 대한 접근성을 높인다.
    클로저 매개변수의 이름은 상위 함수의 매개변수 이름처럼 지정한다. 호출 위치에서 클로저의 전달인자 레이블은 지원되지 않는다.
    /// Ensure that we hold uniquely-referenced storage for at least `requestedCapacity` elements. 
    /// (적어도 `requestedCapacity` element에 대해 uniquely-referenced 저장공간을 확보함을 보장한다.)
    ///
    /// If more storage is needed, `allocate` is called with `byteCount` equal to the number of maximally-aligned bytes to allocate. 
    /// (추가 저장공간이 필요하면, maximally-aligned 바이트 개수와 동일한 `byteCount`를 사용하여 `allocate`를 호출한다.)
    ///
    /// - Returns:
    ///   - reallocated: `true` iff a new block of memory was allocated.
    ///   - capacityChanged: `true` iff `capacity` was updated.
    mutating func ensureUniqueStorage(
      minimumCapacity requestedCapacity: Int, 
      allocate: (_ byteCount: Int) -> UnsafePointer<Void>
    ) -> (reallocated: Bool, capacityChanged: Bool)
     
  • unconstrained polymorphism (제약 없는 다형성)은 overload 하면 모호하게 보일 수 있으므로 특히 주의한다. unconstrained polymorphism의 예는 Any, AnyObject, and unconstrained generic 매개변수 등이다.
    struct Array {  - 좋은 예시. overload 시 전달인자 레이블을 지정하여 의미 및 타입을 명확히 한다.
      /// Inserts `newElement` at `self.endIndex`.
      public mutating func append(_ newElement: Element)
    
      /// Inserts the contents of `newElements`, in order, at `self.endIndex`.  <- 주석과 전달인자 레이블이 일치한다. (이처럼 주석은 API 작성자에게 영향을 미친다.)
      public mutating func append(contentsOf newElements: S)
        where S.Generator.Element == Element
    }

    struct Array {  - overload set의 나쁜 예시
      /// Inserts `newElement` at `self.endIndex`.
      public mutating func append(_ newElement: Element)
    
      /// Inserts the contents of `newElements`, in order, at `self.endIndex`.
      public mutating func append(_ newElements: S)
        where S.Generator.Element == Element
    }
    이 메서드는 semantic family (의미 집합)을 형성하며, 처음에는 매개변수 타입이 뚜렷히 구분되는 것처럼 보인다. 
    하지만 Element가 Any 타입인 경우, '1개 element의 타입' 및 'a sequence of elements의 타입'이 동일할 수도 있다.
    
    var values: [Any] = [1, "a"]
    values.append([2, 3, 4])  <- [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4] ?  <- element의 타입이 모호하다.

- Reference

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments