Maintaining a Library in a Swiftly Moving Ecosystem

Maintaining a Library in a Swiftly Moving Ecosystem

API evolution is tricky. You want to continually improve your library with new features and bug fixes, but at the same time, don't want to break your users' existing code. When your library is written in a language that moves as fast as Swift, the choices you need to make as a maintainer can be especially difficult. How do you decide when to drop support for an old language version? When can you begin incorporating exciting new features like the "some" keyword into your public API? In this talk, we will discuss best practices for API evolution, and how you can use Swift language features to gracefully change your library over time.

A0b320a5ad7c553eb90070d1a968aab3?s=128

Kaitlin Mahar

November 01, 2019
Tweet

Transcript

  1. Maintaining a Library in a Swiftly Moving Ecosystem Kaitlin Mahar

    @k_ _mahar
  2. About me •Software engineer at MongoDB since 2017 •Member of

    the Drivers team, which maintains the official client libraries for using MongoDB from many programming languages •Lead developer and maintainer of our Swift driver •Just pitched to SSWG! tinyurl.com/mongodb-pitch @k_ _mahar
  3. What does “maintaining” mean? •Adding features •Removing features •Modifying your

    APIs •Fixing bugs •Addressing security vulnerabilities •Refactoring/internal changes •Adapting to changes in the language/ecosystem
  4. Change is an inevitable part of library maintenance. How can

    we introduce changes without inflicting pain on our users?
  5. 1. Change your API gradually.

  6. public struct Cat { /// The cat's name. public let

    name: String /// Initialize a new cat. public init(name: String) /// Feed the cat one can of food. public func feed() }
  7. let chester = Cat(name: "Chester") chester.feed() let roscoe = Cat(name:

    "Roscoe") roscoe.feed()
  8. We demand more!

  9. How can we support giving cats snacks?

  10. public struct Cat { /// ... /// Feed the cat

    `cans` cans of food. public func feed(cans: Double) } chester.feed(cans: 0.25)
  11. But… this is a breaking change. let chester = Cat(name:

    "Chester") chester.feed() error: missing argument for parameter 'cans' in call c.feed() ^ cans: <#Double#>
  12. Can we make this change in a more gradual manner?

  13. 1a. Use default values for new method parameters when possible.

    public struct Cat { /// ... /// Feed the cat `cans` cans of food. public func feed(cans: Double = 1.0) }
  14. ✅ Allows us to preserve existing behavior /// Feed the

    cat one can of food. public func feed() /// Feed the cat `cans` cans of food. public func feed(cans: Double = 1.0) before after 1a. Use default values for new method parameters when possible.
  15. ✅ Users’ old code will still compile. chester.feed() 1a. Use

    default values for new method parameters when possible.
  16. ✅ Users who want this feature can start using it

    when we release it! chester.feed(cans: 0.25) 1a. Use default values for new method parameters when possible.
  17. User: “Not all cans of cat food are the same

    size!”
  18. public struct Cat { /// ... /// Feed the cat

    `ounces` ounces of food. public func feed(ounces: Double = 6) } chester.feed(ounces: 2)
  19. Still a breaking change! let chester = Cat(name: "Chester") chester.feed(cans:

    0.25) error: incorrect argument label in call (have 'cans:', expected 'ounces:') c.feed(cans: 0.25) ^~~~~ ounces
  20. public struct Cat { /// ... /// Feed the cat

    `cans` cans of food. @available(*, deprecated, message: "Use feed(ounces:) instead.") public func feed(cans: Double = 1.0) /// Feed the cat `ounces` ounces of food. public func feed(ounces: Double = 6) } 1b. Deprecate features you intend to remove later.
  21. 1b. Deprecate features you intend to remove later. warning: 'feed(cans:)'

    is deprecated: Use feed(ounces:) instead. chester.feed(cans: 1) ^ Compiler: User: chester.feed(cans: 0.25)
  22. @available attribute See also: tinyurl.com/swift-attributes @available(swift, deprecated: 4.2, message: "")

    @available(macOS, introduced: 10.14) @available(*, unavailable, renamed: "newName")
  23. See also: tinyurl.com/swift-attributes Enables you to use the compiler to

    communicate information to your users. @available attribute
  24. When do you actually remove the deprecated feature?

  25. 2. Use Semantic Versioning.

  26. Semantic Versioning (“SemVer”) A versioning scheme where the differences between

    two version numbers convey information about what has changed about the library between the corresponding releases.
  27. x.y.z major minor patch

  28. 1.0.0 2.0.0 1.1.0 1.0.1 major minor patch

  29. type of change major minor patch backwards-compatible bug fix ✅

    ✅ ✅ new backwards- compatible functionality ✅ ✅ deprecated functionality ✅ ✅ substantial internal changes ✅ ✅ backwards- incompatible changes ✅
  30. Why use it? It serves as a contract between maintainers

    and users about what version numbers actually mean and what users can expect to change when they upgrade. See also: semver.org
  31. Pre-1.0: no rules! (well, sort of) •Major version zero (0.y.z)

    is for initial development •Tag 1.0 once your API has stablilized •If you are pre-1.0 for a while, consider minor version bumps for breaking changes
  32. Use with Swift Package Manager .package(url: "git-url-here", requirement) exactly 1.0.0

    1.0.0..<1.1.0 1.0.0..<2.0.0 Requirement Satisfied by .exact("1.0.0") .upToNextMinor(from: "1.0.0") .upToNextMajor(from: "1.0.0")
  33. When should you remove a deprecated feature?

  34. Removing a feature 1. Deprecate it in a minor version

    release.* 2. Remove it no sooner than your next major version release. *you may skip step 1, but if so consider marking unavailable in step 2
  35. 3. Add to your API conservatively.

  36. 3a. If it can already be done simply, don’t add

    another way to do it. public func feedCats(_ cats: [Cat]) feedCats(myCats) myCats.forEach { $0.feed() } • Confusing for users. Which should they use? • Greater maintenance overhead
  37. 3b. Do add helpers to eliminate user errors and boilerplate

    on commonly-used code paths. MongoDB "insert" command try db.runCommand( [ "insert": "cats", "documents": [ ["name": "Chester"], ["name": "Roscoe"] ], "writeConcern": ["w": "majority"] ] ) var opts = InsertManyOptions() opts.writeConcern = try WriteConcern(w: .majority) let cats = db.collection("cats") try cats.insertMany( [ ["name": "Chester"], ["name": "Roscoe"] ], options: opts )
  38. – A. Jesse Jiryu Davis “Features are like children: conceived

    in a moment of passion, they must be supported for years to come.” tinyurl.com/jesses-talk
  39. 3c. When in doubt, leave it out. • By default,

    use internal / fileprivate / private. • Don't expose implementation details the user doesn't need access to. • It’s much easier to add it later than to remove it later.
  40. 4. Be clear about what you support, and what you

    don’t.
  41. Swift 5.0 Swift 5.1 MacOS 10.15 ✅ ✅ Ubuntu 18.04

    ✅ ✅ If you say you support it, you need to test it!
  42. What should you support? • SSWG graduation requirements • CI

    setup for two latest Swift.org recommended versions of Swift • CI setup for two latest versions of Swift.org recommended Linux distributions • MacOS and Linux tests • Support new GA versions of Swift within 30 days • Test early! • You may decide to support more or less, depending on you and your users’ needs
  43. 5. Help users with words, too.

  44. 5a. Publish release notes. • Describe what has changed in

    a release • When appropriate, provide context on why it changed • Highlight what users upgrading to this release should know • Include links to relevant GitHub issues, pull requests, JIRA tickets, etc. • Helpful for users investigating any new warnings or failures that they encounter after upgrading
  45. 5b. Write migration guides for significant API changes. •Explain rationale

    behind the changes •Include examples of how to accomplish common tasks “the new way” •If it’s possible to automate any of the upgrade process (e.g. using a script) include instructions
  46. 5c. Keep existing documentation and examples up-to-date. •Check as part

    of your release process that any sample repos/projects still compile
  47. In summary… • Add to your API conservatively • Introduce

    changes gradually • Use a combination of semantic versioning, Swift features, and good documentation to help users through the process
  48. Thanks! @k_ _mahar