Implicit parameters...?

I really like what this idea enables. But when presented as implicit, it does not feel Swifty. How about we present your idea with a different terminology:

Lets start by looking at what we can already do with default parameter values:

class Context { /* ... */ }

var context = Context()

struct Shape {
    func draw(into context: Context = context) { /* ... */ }
    //                                ^^^^^^^ default is a global var
}

class DebugContext: Context { /* ... */ }

let shape = Shape()

do {
    shape.draw() // Draws to a `Context`
    let currentContext = context; defer { context = currentContext }
    context = DebugContext()
    shape.draw() // Draws to a `DebugContext`
}

This has serious issues, but we can improve it by defining a new kind of default parameter value. I call it a declared default, where we declare the identifier as representing the default value instead of providing an expression. Here is how it would look:

class Context { /* ... */ }

struct Shape {
    func draw(into context: Context = default )  // `context` identifier is declared to represent the default value
       { /* ... */ }                             // This syntax eliminates "What about default for implicit parameter?" question.
}

class DebugContext: Context { /* ... */ }

let default context = Context() // Only `let` binding; override by shadowing in scope

do {
    let shape = Shape()
    shape.draw() // uses the default `context` from outer scope. if no default was declared, the `context` parameter would be required.
    let default context = DebugContext()
    shape.draw() // draws to the `DebugContext` from local scope
    // We can also explicitly provide the parameter:
   let otherContext = Context
   shape.draw(into: otherContext)
}
shape.draw() // back to using the `Context` from global scope, just like any identifier.

I would also want to let protocols declare default identifiers for parameter values:

protocol Shape {
    func draw(into context: Context = default ) 
}

What do you think?

3 Likes

I like that train of thought. Couldn't it be even more simple? For example:

// not current Swift ahead:
// there is no global variable `defaultContext`...
// despite of this the following compiles just fine!
func foo(context: Context = defaultContext) {
}

foo(context: Context()) // ✅
foo() // 🛑 "defaultContext" variable is undefined

func test() {
    var defaultConext = Context()
    foo() // ✅
}

class C {
    var defaultConext = Context()
    func bar() {
        foo() // ✅
    }
}

Probably too late for Swift as that would be a breaking change, but I do like it for its simplicity and obviousness.†


† Edit: we could make a slight syntax addition to make this happen in current swift in a compatible manner, for example:

func foo(context: Context = BIKE_SHED_NAME_HERE defaultContext) {
}

BIKE_SHED_NAME_HERE modifies how the expression is being parsed hence putting it in that position near the expression. Some candidates for it: implicit, lazy (to reuse the keyword), execute, evaluate, var. The meaning is: use this name from the "current" (at the point of use) scope.


Ditto for @wadetregaskis 's operator example:

func == (_ a: T, _ b: T, epsilon: Double = theEpsilon) { … }

a == b // 🛑 no `theEpsilon` variable defined in the current context
(==)(a, b, epsilon: 1) // ✅

func test() {
    let theEpsilon = 1.0
    a == b // ✅
}

On the bright side it would allow things like these:

func == (a: Double, b: Double, file: String = #file, line: Int = #line) { … }

That's of course not the worst syntax in the world, and for the sake of having parameters to operators I'd accept it as better than none, but only barely. That syntax is less natural (for arithmetic) and I don't see any real benefit over a regular function at that point, e.g.:

func foo(x: Double, y: Double, i: inout Int) {
    if x.equals(y, epsilon: 0.1) {
        c.add(a, rounding: .up)
    }
}

The difference between my proposed syntax and the existing syntax above is somewhat subtle, I concede, but I like preserving the arithmetic symbols. In a lot of everyday code it's pretty irrelevant, because you don't do a lot of mathematics, but when you do get really into a non-trivial numerical algorithm it really does matter to have clean syntax that uses symbols and conventions of written mathematics.

Tangential to the basic point being made here, but I want to clarify that we would never, ever accept a == spelling for an operation that was not actual equality. Approximate equality checks are useful, but it's really vital that they are obviously not the same as equality.

8 Likes

Yep, I understand. But in mathematics you don't actually specify epsilon as a parameter for an equality operator. You write |x - y| ⩽ ε, or you use another operator, like x ≅ y

True, but assumptions like "x - y is well-defined and/or won't crash" don't work in Swift (for numeric types broadly) so you can't write it that way, and I'm totally fine with or similar notation but you still need a way to pass a (non-operand) parameter to it.

That's already not true:

struct Record: Equatable {
    let id: String
    let someOtherData: String

    static func == (lhs: Record, rhs: Record) -> Bool {
        return lhs.id == rhs.id
    }
}

Just because two things are considered equal doesn't mean they're identical. What equality means is entirely up to the author of the type (and can be parameterised already, e.g. String.compare(_:options), just not as succinctly as with operator syntax).

The language permits you to write all sorts of things that we would never consider doing in the libraries provided by the swift organization.

The example shown here is quite different from numeric comparison with a tolerance, however--comparing only on a subset of the fields is unfortunate in that it makes equality not imply substitutability, but it does not violate the basic axioms that algorithms written against Equatable depend on. Comparison with a tolerance violates transitivity of equality, which breaks all sorts of generic algorithms that one might write.

3 Likes

DistributedActor does this. It defines equality in terms of its id property but it also has actorSystem and unownedExecutor properties. I'd wager that a lot of Identifiable types are like that (I've certainly seen heaps involving SwiftUI, although I can't say if Apple's SwiftUI code does this since it's all closed-source).

It's pedantic, but technically Array and Set do too, as they compare based on populated contents without considering their capacities. Nominally their capacities are irrelevant but in practice it can matter (e.g. to performance). The definers of those types chose to exclude that consideration, which I think was the right decision. Nonetheless, two such collections can be considered equal without being identical.

This is of course of limited relevance to the example at hand, with numeric operators, but your assertion made me curious. From a partial survey, it's true that most types in the Swift Stdlib do essentially do a bytewise comparison for ==.

It can, yes. But two considerations:

  • It's not clear anyone's suggesting numeric equality be [intentionally] fuzzy by default. I certainly wasn't.

    Admittedly it does get murky with implicit parameters as maybe that'd effectively override that default at a large scope (with very little indication it's doing so)…?

  • Fixed-precision numerics are inherently fuzzy, because the result of various operations has to be truncated or otherwise rounded to fit within their limited precision. e.g. Swift mistakenly says 100 - 99.9 != 10 - 9.9 (mistaken from a pure mathematical sense, of course - it is explicable why it comes to this conclusion).

    That is in fact why approximate comparison of such values is often needed, to prevent a logically incorrect conclusion about the outcome of an algorithm.

Ultimately, though, I just don't get why equality written as == has to be different to equality written as isEqual or whatever. It's the same net result, functionally, and what equality means can always be context-specific and variable.

This has been suggested several times on these forums over the years, and it has always been a bad idea. Implicit parameters would surely be used to do so by some folks =). That's fine, but I definitely don't want it to be a blessed precedent.

No. Floating-point is not inherently fuzzy. It inherently rounds, and therefore differs from real arithmetic, but under IEEE 754 rules, there is a well-defined numeric result for all basic arithmetic operations. In floating-point arithmetic, those two values are not equal, and Swift correctly identifies that. Treating them as equal would be a bug.

I wouldn't write it as isEqual either. Predicates that use approximate equality should be clearly identified as such, because not being transitive violates fundamental expectations people have for "equal" in a way that omitting some information from the comparison does not.¹

For example, in Numerics, we call these operations isApproximatelyEqual(to:[various optional args]), to make it explicit that this is not an equality operation.


¹ In an ideal world, we would have formalized this concept more carefully in Equatable; I would have liked to see some notion of formally separating the semantic value and representation. Two arrays with different capacity might have the same value, but different representations. Ditto +0.0 and -0.0. But we have to live with history.

3 Likes

i stopped doing this a long time ago, because it really inhibits testing any APIs that traffic in Record.

Well, that's what I was alluding to with it being explicable. We (as experienced programmers) are well-aware of concept vs reality when it comes to, well, many things. Finite-precision maths is one of those.

I get where you're coming from, and I understand your semantics, but I think my point was missed. A regular person would call Swift wrong on that example, as would any theoretical mathematician (though they might know enough about finite representations, or just the practicalities of dealing with computers, to understand why Swift got it wrong). We (as Swift programmers) can say "no, it's not wrong, that's how IEEE 754 works" but that's kind of just a rationalisation.

And all this is obviously not specific to Swift, most programming languages use fixed-precision numerics for reals. I'm not picking on Swift here, it's merely the language we're talking about.

Anyway, to wrap up this tangent, I think we understand each other, we just have different opinions about what's acceptable syntax for this functionality. Swift Stdlib doesn't have to provide a a ≅ b (epsilon:) operator, but it'd be cool if I could in my own code.

P.S. How do you do footnotes?!

2 Likes

to me, this is not a question of “does the language support this”, it is a question of “where should my code live?”

to me, second-tier operands are interesting for passing contexts to DSL-shaped things, like HTML builders. syntactically, this can be accomplished by making the RHS a tuple type:

return .html 
{
    $0[.p]
    {
        $0[.a] { $0.href = self.url(id) } = (id, self.localizations)  
    }
}

but there’s no place the operator implementation could possibly live except as a subscript overload on the encoding target, $0. which is a terrible place to put it, from a code organization standpoint.

You can pretty much make it work it today, but I wouldn't suggest that anyone do so. Just for kicks, I made a numerics branch a few years back that lets you write a ≈ b ± tol and the like:

Obligatory "your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should"

2 Likes

for “real world” use, i would prefer tuples over trinary operators because you can have many different kinds of contexts and you can give a tuple element a label. (and you don’t need to boilerplate a new type for every combination of (b, c) inputs!)

perhaps Pitch: User-defined tuple conformances will open up some new possibilities.

1 Like

It’s not a real footnote, I just use the Unicode superscript numerals and a markdown hline.

1 Like

Well, I think it's brilliant.

XCTAssert(zero ≈ zero ± tol)

…is so much easier to read than:

XCTAssertTrue(zero.isApproximatelyEqual(to: zero, absoluteTolerance: tol))

Though the former doesn't have a way to specify tol through an implicit parameter, as far as I can tell…?

Still, it's an interesting real-world-code case study; imagine if instead of ± tol there were just an implicit keyword (or somesuch) on the tol parameter. I'm not thrilled with the result - it feels rather obtuse that some parameter (or equivallently, local constant) could influence all comparison operators in these unit tests.

(the point holds for the plain function version as well)

Perhaps if it had a name that could become canonical and well-known, then at least an experienced Swift programmer might recognise what's going on. But I'm not thrilled with that 'cost of entry', so to speak.

Also, with the proposed implicit parameters how would one distinguish between "this parameter may be provided implicitly" and "use this parameter to fill implicit parameters in functions I call"? Unless I missed something, I'm not seeing any new keyword than a single implicit in the examples so far in this thread, and all the examples seem to imply you have to copy the argument into an implicit local variable in order to effect further propagation?

Did you see my proposed terminology above?

Using local variables (marked or unmarked as in the sketch above) as configuration parameters is very powerful and works in various contexts including threads, async await, etc, although I wouldn't preclude using other scoped variables (e.g. an instance variable of the current or parent class, or a file private variable, etc).