Як ще використовувати type safety в цілях поліпшення API

Всім привіт! Я — lead developer cocos2d-objc. Зараз ми знаходимося в процесі портування на Swift. Я планую висвітлювати процес розробки, розповідати архітектурні рішення і т. д. Поки що проект ще на proof-of-a-concept стадії, тому сьогодні я розповім тільки про маленькому прийомі, який, як я вважаю, зробив нашу математичну бібліотеку трохи краще. Якщо цікаво — прошу під кат.
image


Перед тим, як почати переписувати движок на Swift, стала очевидною потреба в сучасній бібліотеці під математичні потреби. Движок спочатку x-platform in mind, так що CG* типи ми використовувати не могли, та й CoreGraphics API недостатньо Swifty для нас. Існуючі рішення нас не задовольняли, тому ми вирішили написати свій велосипед, при цьому дотримуючись певного аскетизму.

Ми обмежилися скромним набором типів: Vector2f, Vector3f, Vector4f, Matrix3f, Matrix4f, Rect. Ми твердо вирішили, що ми хочемо повністю виключити ARC overhead і точно хочемо мати підтримку SIMD (хоча б на Darwin платформах, для Glibc поки що алгоритми прописані вручну, до тих пір, поки simd не буде доступний публічно), з цієї причини довелося відмовитися від дженериків і зав'язати всю бібліотеку на типі Float, без підтримки Double.

Ок, я зрозумів, що вам це не цікаво, це вступ. Про що стаття?

В якийсь момент ми зрозуміли, що API, яке працює з кутами (зразок методів створення матриці повороту і т. д.) потребує вдосконалення. Проблема, яка здавалася нам важливою — прибрати необхідність користувачам дивитися документацію на предмет того, що очікує отримати метод: радіани чи градуси.

Спочатку ми хотіли зробити аліаси на Float виду
typealias Radians = Float; typealias Degrees = Float;


Ясна річ, що це особливо не рятує, тому що все одно є ймовірність передати в метод значення не тією величиною.

Також ми розглядали створення функцій rad) і deg), які б повертали потрібне значення. Варіанти розширень Int і Float, а також їх літералів виду
45.degrees
180.radians

Нам не подобалися, тому що дозволяли робити:
180.radians.radians

У підсумку було прийнято рішення створити окрему структуру під тип Angle:
/// A floating point value that represents an angle
public struct Angle {

/// The value of the angle in degrees
public let degrees: Float

/// The value of the angle in radians
public var radians: Float {
return degrees * Float.pi / 180.0
}

/// Creates an instance using the value in radians
@inline(__always)
public init(radians val: Float) {
degrees = val / Float.pi * 180.0
}

/// Creates an instance using the value in degrees
@inline(__always)
public init(degrees val: Float) {
degrees = val
}

@inline(__always)
internal init(_ val: Float) {
degrees = val
}
}


Чим це добре? Тепер ми можемо сказати користувачеві, що метод очікує кут в якості параметра, причому йому не потрібно турбуватися, в якому саме вигляді: в радіанах або в градусах.
Якщо він передає структуру Angle, то він впевнений, що все буде працювати коректно.

Ми визначили всі стандартні оператори для роботи з Angle (такі ж, як для скалярних величин, тільки Angle / Angle повертає Float замість Angle, а Angle * Angle немає зовсім)

Також ми вирішили залишити extension для Int:
extension Int {
/// Returns the integer value as an angle in degrees
public var degrees: Angle {
return Angle(degrees: Float(self))
}
}


Таким чином, ми оперуємо нашими кутами в градусах, не втрачаючи точність там, де не треба і переводимо їх в радіани тільки тоді, коли необхідно (зазвичай, при кінцевих обчисленнях).
Щоб ще більше забезпечити точність, ми визначили sin і cos для Angle ось так:

@inline(__always)
internal func sinf(_ a: Angle) -> Float {
return __sinpif(a.degrees / 180.0)
}

@inline(__always)
internal func cosf(_ a: Angle) -> Float {
return __cospif(a.degrees / 180.0)
}


Правда, для Glibc довелося написати звичайну імплементацію цих фукнций, т. к. там немає функцій підвищеної точності.

Ну і наостанок: використання юнікоду в коді — завжди спірна тема. Особисто я зовсім не вітаю це. Спочатку, ми заради веселощів додали ось такий оператор:

/// The degree operator constructs an `Angle` from the specified floating point value in degrees
///
/// - remark: 
/// * Degree operator is the unicode symbol U+00B0 DEGREE SIGN
/// * macOS shortcut is ⌘+⇧+8
@inline(__always)
public postfix func °(lhs: Float) -> Angle {
return Angle(degrees: lhs)
}

/// Constructs an `Angle` from the specified `Int` value in degrees
@inline(__always)
public postfix func °(lhs: Int) -> Angle {
return Angle(degrees: Float(lhs))
}


І визначили наші константи ось так:
// MARK: Constants
public static let zero = 0°
public static let pi_6 = 30°
public static let pi_4 = 45°
public static let pi_3 = 60°
public static let pi_2 = 90°
public static let pi2_3 = 120°
public static let pi = 180°
public static let pi3_2 = 270°
public static let pi2 = 360°


У підсумку, я зловив себе на тому, що використовував цей оператор у коді самого движка, читабельності це додає, однак не варто зловживати — не всі пам'ятають шорткаты і деколи це бісить.

Стаття вийшла дуже великий, для досить дріб'язкового рішення, проте я сподіваюся вам було цікаво. Застосовуючи цей прийом, ми вирішили проблему, коли потрібно ганяти туди-сюди значення і читати документацію на предмет того, в яких величинах очікується значення.

Стежити за портированием движка тут.
Посилання на math lib.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.