String Comparison (was: Strings in Swift 4)

Breaking this into a different thread…

One suggestion which I haven’t seen (maybe it is too crazy) is the idea of introducing shadowing of operators within a scope (similar to variables).

Now this would need a TON of design work to make it actually work, but the gist would be:

  if blah == foo {
    //We can override/shadow the ‘<‘ operator within this scope as long as the signature is the same
    let (<) = String.comparisonOperation(case: .insensitive, locale: .current) //Here we grab a closure produced by a static function on String
    
    if a < b { //Within this scope < will be case insensitive
      //Stuff
    }
  }
  //Outside of the scope, < is back to it’s lovable self

Thoughts on the general idea?

Thanks,
Jon

···

I know you want to defer this for now, so feel free to set this part of the email aside, but here's a quick list of solutions I've ballparked:

1. Your "one operand carries the options" solution.

2. As I mentioned, do something that effectively overloads comparison operators to return them in a symbolic form. You're right about the ambiguity problem, though.

3. Like #2, but with slightly modified operators, e.g.:

  if localized(fu &< br, case: .insensitive) { … }

4. Reintroduce something like the old `BooleanType` and have *all* comparisons construct a symbolic form that can be coerced to boolean. This is crazy, but actually probably useful in other places; I once experimented with constructing NSPredicates like this.

  protocol BooleanProtocol { var boolValue: Bool { get } }
  
  struct Comparison<Operand: Comparable> {
    var negated: Bool
    var sortOrder: SortOrder
    var left: Operand
    var right: Operand
    
    func evaluate(_ actualSortOrder: SortOrder) -> Bool {
      // There's circularity problems here, because `==` would itself return a `Comparison`,
      // but I think you get the idea.
      return (actualSortOrder == sortOrder) != negated
    }
  }
  extension Comparison: BooleanProtocol {
    var boolValue: Bool {
      return evaluate(left.compared(to: right))
    }
  }
  
  func < <ComparableType: Comparable>(lhs: ComparableType, rhs: ComparableType) -> Comparison {
    return Comparison(negated: false, sortOrder: .before, left: lhs, right: rhs)
  }
  func <= <ComparableType: Comparable>(lhs: ComparableType, rhs: ComparableType) -> Comparison {
    return Comparison(negated: true, sortOrder: .after, left: lhs, right: rhs)
  }
  // etc.
  
  // Now for our special String comparison thing:
  func localized(_ expr: Comparison<String>, case: StringCaseSensitivity? = nil, …) -> Bool {
    return expr.evaluate(expr.left.compare(expr.right, case: case, …))
  }

5. Actually add some all-new piece of syntax that allows you to add options to an operator. Bad part is that this is ugly and kind of weird; good part is that this could probably be used in other places as well. Strawman example:

  // Use:
  if fu < br %(case: .insensitive, locale: .current) { … }
  
  // Definition:
  func < (lhs: String, rhs: String, case: StringCaseSensitivity? = nil, …) -> Bool { … }

6. Punt on this until we have macros. Once we do, have the function be a macro which alters the comparisons passed to it. Bad part is that this doesn't give us a solution for at least a version or two.

I agree that there are refinements to operators that are needed, such as
the ability to pick which ones to import and more flexibility in shadowing
them, etc. But I think that's a whole nother discussion, and I rather think
that string comparison is a distinctly poor use case for it. An operator is
really tailored to do one unambiguous operation with two arguments. Here,
there's more than one operation when it comes to string comparison and
therefore multiple arguments. Why try to fit a square peg into a round
hole? It's already possible to do what you suggest with ordinary function
names, so what's different here seems to come down to new syntax that IMO
should be designed separately in the context of operators, not really
something to do with strings.

···

On Tue, Jan 24, 2017 at 03:13 Jonathan Hull via swift-evolution < swift-evolution@swift.org> wrote:

Breaking this into a different thread…

One suggestion which I haven’t seen (maybe it is too crazy) is the idea of
introducing shadowing of operators within a scope (similar to variables).

Now this would need a TON of design work to make it actually work, but the
gist would be:

        if blah == foo {
                //We can override/shadow the ‘<‘ operator within this
scope as long as the signature is the same
                let (<) = String.comparisonOperation(case: .insensitive,
locale: .current) //Here we grab a closure produced by a static function on
String

                if a < b { //Within this scope < will be case
insensitive
                        //Stuff
                }
        }
        //Outside of the scope, < is back to it’s lovable self

Thoughts on the general idea?

Thanks,
Jon

> I know you want to defer this for now, so feel free to set this part of
the email aside, but here's a quick list of solutions I've ballparked:
>
> 1. Your "one operand carries the options" solution.
>
> 2. As I mentioned, do something that effectively overloads comparison
operators to return them in a symbolic form. You're right about the
ambiguity problem, though.
>
> 3. Like #2, but with slightly modified operators, e.g.:
>
> if localized(fu &< br, case: .insensitive) { … }
>
> 4. Reintroduce something like the old `BooleanType` and have *all*
comparisons construct a symbolic form that can be coerced to boolean. This
is crazy, but actually probably useful in other places; I once experimented
with constructing NSPredicates like this.
>
> protocol BooleanProtocol { var boolValue: Bool { get } }
>
> struct Comparison<Operand: Comparable> {
> var negated: Bool
> var sortOrder: SortOrder
> var left: Operand
> var right: Operand
>
> func evaluate(_ actualSortOrder: SortOrder) -> Bool {
> // There's circularity problems here, because `==`
would itself return a `Comparison`,
> // but I think you get the idea.
> return (actualSortOrder == sortOrder) != negated
> }
> }
> extension Comparison: BooleanProtocol {
> var boolValue: Bool {
> return evaluate(left.compared(to: right))
> }
> }
>
> func < <ComparableType: Comparable>(lhs: ComparableType, rhs:
ComparableType) -> Comparison {
> return Comparison(negated: false, sortOrder: .before,
left: lhs, right: rhs)
> }
> func <= <ComparableType: Comparable>(lhs: ComparableType, rhs:
ComparableType) -> Comparison {
> return Comparison(negated: true, sortOrder: .after, left:
lhs, right: rhs)
> }
> // etc.
>
> // Now for our special String comparison thing:
> func localized(_ expr: Comparison<String>, case:
StringCaseSensitivity? = nil, …) -> Bool {
> return expr.evaluate(expr.left.compare(expr.right, case:
case, …))
> }
>
> 5. Actually add some all-new piece of syntax that allows you to add
options to an operator. Bad part is that this is ugly and kind of weird;
good part is that this could probably be used in other places as well.
Strawman example:
>
> // Use:
> if fu < br %(case: .insensitive, locale: .current) { … }
>
> // Definition:
> func < (lhs: String, rhs: String, case: StringCaseSensitivity? =
nil, …) -> Bool { … }
>
> 6. Punt on this until we have macros. Once we do, have the function be a
macro which alters the comparisons passed to it. Bad part is that this
doesn't give us a solution for at least a version or two.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

+1

···

on Tue Jan 24 2017, Xiaodi Wu <xiaodi.wu-AT-gmail.com> wrote:

I agree that there are refinements to operators that are needed, such as
the ability to pick which ones to import and more flexibility in shadowing
them, etc. But I think that's a whole nother discussion, and I rather think
that string comparison is a distinctly poor use case for it. An operator is
really tailored to do one unambiguous operation with two arguments. Here,
there's more than one operation when it comes to string comparison and
therefore multiple arguments. Why try to fit a square peg into a round
hole? It's already possible to do what you suggest with ordinary function
names, so what's different here seems to come down to new syntax that IMO
should be designed separately in the context of operators, not really
something to do with strings.

--
-Dave