Skip to content

Instantly share code, notes, and snippets.

@groue
Last active March 13, 2021 15:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save groue/479725ec80a84b8343c5fa5c50b865cc to your computer and use it in GitHub Desktop.
Save groue/479725ec80a84b8343c5fa5c50b865cc to your computer and use it in GitHub Desktop.
Pitch: Scoped Functions

Pitch: Scoped Functions

Introduction

Scoped Functions are functions that enhance the leading dot syntax inside their body, so that it gives short-hand access to the members of a specific value. Such functions exist so that API designers and their consumers can collaborate towards a way to write focused code where an agreed implicit and obvious context does not have to be spelled out.

Motivation

This pitch considers that developers often want to use short-hand identifier resolution in particular contexts, and api designers often want to make it possible.

The Swift language already provides much help in this regard. We have the implicit self, the current (and evolving) rules for the leading dot syntax, the scoping rules for accessing nested types, etc.

But developers always want more:

In all those examples, developers express a desire to enhance identifier resolution in a lexically scoped context: a value, or the body of a closure, where both are arguments of a function.

Considering:

  • The braces of a closure body are a well delimited lexical scope.
  • A function call is a strong semantic signal that is able to define an implicit and obvious context that both api designers and consumers can agree on.
  • Naked identifier resolution in Swift is already pretty busy with local variables, self, type names, module names, etc, so we do not propose modifying this.
  • The leading dot syntax is a beloved short-hand syntax of Swift.

We modify the rules that govern leading dot syntax inside the body of a particular closure argument. When the closure is an autoclosure, the leading dot syntax is modified for the value that feeds this autoclosure.

This gives:

// The build(_:configure:) function accepts a closure 
// scoped on its first argument:
let label = build(UILabel()) {
    .text = "Hello scoped functions!"
    .font = .preferredFont(forTextStyle: .body)
}

// The `filter(_:)` and `order(_:)` functions accept an 
// autoclosure scoped on the generic type of the request
// (here, `Player`).
// `Player` collaborates, and has defined two
// `Player.score` and `Player.team` static constants.
let players = try Player.all()
    .filter(.team == "Red")
    .order(.score.desc)
    .limit(10)
    .fetchAll(from: database)

// A revisit of SE-0299: the argument of toggleStyle(_:) is
// an autoclosure scoped on an ad-hoc enum type:
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
  .toggleStyle(.switch)

Without scoped functions, developers face various problems:

// The `label` variable is repeated.
let label = UILabel()
label.text = "Hello World!"
label.font = .preferredFont(forTextStyle: .body)

// The `Player` type is repeated.
let players = try Player.all()
    .filter(Player.team == "Red")
    .order(Player.score.desc)
    .limit(10)
    .fetchAll(from: database)

// As before SE-0299
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
  .toggleStyle(SwitchToggleStyle())

Personal motivation: as the author of GRDB, which defines a type-safe API for building SQL queries, and relies of static properties defined on user-defined "record types", I'd love to simplify some user-land code:

// CURRENT GRDB CODE
struct Team: Codable, FetchableRecord {
    static let players = hasMany(Player.self)
    static let awards = hasMany(Award.self, through: players, using: Player.awards)
}
struct Player: Codable, FetchableRecord {
    static let awards = hasMany(Award.self)
}
struct Award: Codable, FetchableRecord {
}

// All red teams with their awarded players
let request = Team
  .filter(Team.Columns.name == "Red")
  .including(all: Team.players.having(Player.awards.count > 0))

// WITH SCOPED FUNCTIONS
struct Team: Codable, FetchableRecord {
    static let players = hasMany(Player.self)
    static let awards = hasMany(Award.self, through: players, using: .awards) // <-
}
struct Player: Codable, FetchableRecord {
    static let awards = hasMany(Award.self)
}
struct Award: Codable, FetchableRecord {
}

// All red teams with their awarded players
let request = Team
  .filter(.name == "Red")                             // <-
  .including(all: .players.having(.awards.count > 0)) // <-

Sorry if I provided a contrieved example. But contrieved examples are precisely those where unnecessary clutter obscures the intent the most.

Proposed solution

A new function attribute is introduced: @scoped. The @scoped attribute can only be used for functions that accept at least one argument.

@scoped f(T, ...) -> ... is a scoped function, and T is the scope type. The first argument is the scope value. Inside the body of the function, the scope value feeds the leading dot syntax. The function is said to be T-scoped.

For example:

let f: @scoped (String) -> Int // A
f = { return .count }          // B
f("Hello")                     // C: prints "5"
  • In (A), f is defined as a String-scoped function.

  • In (B), the compiler is able to resolve .count as String.count.

  • In (C), the function is called with a particular String.

We are able to address some of the above examples:

  1. The build(_:_:) function

    func build<T>(_ initial: T, update: @scoped (inout T) throws -> ()) rethrows -> T {
        var value = initial
        update(&value)
        return value
    }
    
    let label = build(UILabel()) {
        .text = "Hello scoped functions!"
        .font = .preferredFont(forTextStyle: .body)
    }
    label.text // "Hello"

    Woot, we'll have to define how the compiler deals with leading dot syntax for .font, .preferredFont, and .body :-)

  2. The database query

    struct Request<T> {
        func filter(_ expression: @autoclosure @scoped (T.Type) -> Expression) -> Self { ... }
        func order(_ ordering: @autoclosure @scoped (T.Type) -> Ordering) -> Self { ... }
        func limit(_ limit: Int) -> Self { ... }
    }
    
    extension Player {
        static let team = Column(...)
        static let score = Column(...)
    }
    
    let request: Request<Player> = ...
    request.filter(.team == "Red").order(.score.desc)

    The @autoclosure qualifier has the compiler change the leading dot scoping for values.

Detailed design

TBD

Source compatibility

Scoped function are an additive feature that creates no source compatibility issue.

Effect on ABI stability

A scoped function, at runtime, is a plain function that accepts its scope as the first argument. Its a compiler-only feature, without any effect on the ABI.

Effect on API resilience

TBD

Alternatives considered

Use another sigil than . ? For example, ' or ..:

let label = build(UILabel()) {
    'text = "Hello scoped functions!"
    'font = .preferredFont(forTextStyle: .body)
}

let label = build(UILabel()) {
    ..text = "Hello scoped functions!"
    ..font = .preferredFont(forTextStyle: .body)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment