Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Declarative Traits - Swift Bangalore #9

Eeb061c8b2816b771920da1b3e7904a3?s=47 Swift India
September 22, 2018

Declarative Traits - Swift Bangalore #9

Eeb061c8b2816b771920da1b3e7904a3?s=128

Swift India

September 22, 2018
Tweet

Transcript

  1. Declarative Traits … what?

  2. Declarative programming recap • All about describing the “what”, not

    the “how” • Express logic, not control flow • Abstracts between description and implementation • Code that takes 0 mental cost to understand • What you see is what you get
  3. What is this talk about? • Creating a declarative API

    • Creating a modular API • Using Swift’s generic capabilities to express a concept in a new way
  4. FileHandle • Class from the Foundation framework • Used to

    “to access data associated with files, sockets, pipes, and devices”
  5. Example usage import Foundation var desktop = URL(fileURLWithPath: "/Users/vmanot/Desktop") var

    fileURL = desktop.appendingPathComponent("test.txt") var reading = try FileHandle(forReadingFrom: fileURL) var writing = try FileHandle(forWritingTo: fileURL) var updating = try FileHandle(forUpdating: fileURL)
  6. Read/write/update // Read _ = reading.readDataToEndOfFile() // Write writing.truncateFile(atOffset: 0)

    // reset file writing.write("new text".data(using: .utf8)!) // Update var data = updating.readDataToEndOfFile() var string = String(data: data, encoding: .utf8)! string += " with more new text" let newData = string.data(using: .utf8)! updating.seek(toFileOffset: 0) updating.write(newData)
  7. Problem with this API • Can’t tell whether FileHandle is

    for reading, writing, or updating • Runtime error when read-only handle attempts to write • No static enforcement • Reading from write-only handle silently errors by returning 0-length data
  8. How can we improve it? • Wrappers • One for

    each type of operation • Restrict operations at compile-time
  9. Red class ReadOnlyHandle { var value: FileHandle init(url: URL) throws

    { value = try .init(forReadingFrom: url) } func seek(toFileOffset offset: UInt64) { return value.seek(toFileOffset: offset) } func readDataToEndOfFile() -> Data { return value.readDataToEndOfFile() } }
  10. Write class WriteOnlyHandle { var value: FileHandle init(url: URL) throws

    { value = try .init(forReadingFrom: url) } func seek(toFileOffset offset: UInt64) { return value.seek(toFileOffset: offset) } func write(_ data: Data) { value.write(data) } } =
  11. Update class UpdateOnlyHandle { var value: FileHandle init(url: URL) throws

    { value = try .init(forReadingFrom: url) } func seek(toFileOffset offset: UInt64) { return value.seek(toFileOffset: offset) } func readDataToEndOfFile() -> Data { return value.readDataToEndOfFile() } func write(_ data: Data) { value.write(data) } }
  12. Pros of this approach • Meaning is apparent in type

    • Can effectively restrict read/write/update operations at compile-time • Prevents explicit/silent runtime errors
  13. Cons of this approach • Too many classes • Subclass

    hell (each class must be specialized) • Duplicate code (for e.g. seeking to a file offset)
  14. Solution • Generic parametrization via “traits” • Make read/write/update-ability a

    generic parameter • Use where-clauses to conditionally implement functionality
  15. 1. Define an enum enum FileAccessMode { case read case

    write case update }
  16. 2. Define protocols protocol FileAccessModeType { static var value: FileAccessMode

    { get } } protocol FileReadAccessModeType: FileAccessModeType { } protocol FileWriteAccessModeType: FileAccessModeType { } protocol FileUpdateAccessModeType: FileReadAccessModeType, FileWriteAccessModeType { }
  17. 3. Define types struct ReadAccess: FileReadAccessModeType { static let value:

    FileAccessMode = .read } struct WriteAccess: FileWriteAccessModeType { static let value: FileAccessMode = .write } struct UpdateAccess: FileUpdateAccessModeType { static let value: FileAccessMode = .update }
  18. 4. The class class NewFileHandle<AccessMode: FileAccessModeType> { var value: FileHandle

    init(url: URL) throws { switch AccessMode.value { case .read: value = try .init(forReadingFrom: url) case .write: value = try .init(forWritingTo: url) case .update: value = try .init(forUpdating: url) } } }
  19. 5. Implement functionality extension NewFileHandle { func seek(toFileOffset offset: UInt64)

    { return value.seek(toFileOffset: offset) } } extension NewFileHandle where AccessMode: FileReadAccessModeType { func readDataToEndOfFile() -> Data { return value.readDataToEndOfFile() } } extension NewFileHandle where AccessMode: FileWriteAccessModeType { func write(_ data: Data) { value.write(data) } }
  20. Finally var read: NewFileHandle<ReadAccess> var write: NewFileHandle<WriteAccess> var update: NewFileHandle<UpdateAccess>

  21. What did we do • Moved the access mode of

    the handle to the type declaration as a “trait”. A trait here is just basically a type that wraps a static value. • Read this trait and performed custom logic. • Implemented protocols for these traits to allow scalable where-clauses.
  22. Benefits of this approach • One type to rule them

    all! • No code duplication for “seek” function • No code duplication for read/write for update access mode • Can be subclassed without said code duplication
  23. Another example! • Strings can be encoded using UTF8, UTF16

    and UTF32. • Subclass NewFileHandle to handle reading/writing/ updating text • Parametrize with encoding
  24. Protocol & types protocol UTFEncodingType { static var value: String.Encoding

    { get } } extension UTF8: UTFEncodingType { static var value: String.Encoding = .utf8 } extension UTF16: UTFEncodingType { static var value: String.Encoding = .utf16 } extension UTF32: UTFEncodingType { static var value: String.Encoding = .utf32 }
  25. Class class TextFileHandle<Encoding: UTFEncodingType, AccessMode: FileAccessModeType>: NewFileHandle<AccessMode> { }

  26. Functionality extension TextFileHandle where AccessMode: FileReadAccessModeType { func readTextToEndOfFile() ->

    String? { return String(data: readDataToEndOfFile(), encoding: Encoding.value) } } extension TextFileHandle where AccessMode: FileWriteAccessModeType { func write(_ text: String) { guard let data = text.data(using: Encoding.value) else { return } write(data) } }
  27. Usage var read8: TextFileHandle<UTF8, ReadAccess> var read16: TextFileHandle<UTF16, ReadAccess> var

    read32: TextFileHandle<UTF32, ReadAccess> var write8: TextFileHandle<UTF8, WriteAccess> var write16: TextFileHandle<UTF16, WriteAccess> var write32: TextFileHandle<UTF32, WriteAccess> var update8: TextFileHandle<UTF8, UpdateAccess> var update16: TextFileHandle<UTF16, UpdateAccess> var update32: TextFileHandle<UTF32, UpdateAccess>
  28. Notable points • 9 classes would be required without parametrization!

    • Code reads cleaner • Code is scalable and can be subclassed further without specifying encoding
  29. Potential • Injecting code through generic parametrization (for e.g. a

    protocol that specifies a visual effect that can be injected into a UIView subclass) • Cosmetic annotations, for serving no real purpose but to write self-documenting type declarations
  30. Cons • Can’t override functions in extensions (…yet) • Creating

    protocols might be tedious • Creating the placeholder types might be tedious
  31. Thank You

  32. Vatsal Manot iOS Engineer at DocTalk Solutions Twitter: @vatsal_manot