[Discussion][Expericment] D-like slicing syntax


(Daniel Duan) #1

Hi all,

A while ago there's a thread on adding Python's slicing syntax here:
http://thread.gmane.org/gmane.comp.lang.swift.evolution/124/focus=261

From it, a brief summary of the issues in Python's slicing syntax:

1. light syntax may suggest O(1) operation complexity to some.
2. negative index imposes runtime overhead
3. negative index masks fencepost errors (`foo(m-n)` supprises if you assume
   m > n)

D's slicing syntax (https://dlang.org/d-array-article.html) was brought up as
an alternative. It does a better job in suggesting "unusual" operation (if you
treat subscripting with an index as normal). I attached a rudimentary
implementation of it at the end. It's in not way aimed at being complete or
ending up in stdlib. Hopefully, being able to play with it will generate more
discussion on this topic :slight_smile:

// (if you prefer a gist: https://gist.github.com/dduan/5c54df865fdd7b6b7548)
//
// Incomplete:
// '%' replaces the more ideal '$' (the latter can't be in name of operator)
// where a open ended range is in the design, `nil` is used its place
// force casting between Index.Distance and 'Int'
// only on CollectionType, but a version for string is not hard to add

public struct Offset { let value: Int }

prefix operator %+ {}
prefix operator %- {}

prefix func %+(offset: Int) -> Offset {
    return Offset(value: offset)
}
prefix func %-(offset: Int) -> Offset {
    return Offset(value: -offset)
}

internal func normalize(start: Int?, end: Int?, total: Int) -> (Int, Int) {
    var actualStart = start ?? 0
    var actualEnd = end ?? total
    actualStart = actualStart < 0 ? total + actualStart : actualStart
    actualEnd = actualEnd < 0 ? total + actualEnd : actualEnd
// uncomment the following for a much more forgiving index behavior
// actualStart = actualStart < 0 ? 0 : actualStart
// actualEnd = actualEnd < 0 ? 0 : actualEnd
// let safeEnd = min(actualEnd, total)
// return (min(actualStart, safeEnd), safeEnd)
    return (actualStart, actualEnd)
}

extension CollectionType {
    public subscript(start:Offset?, end:Offset?) -> Self.SubSequence {
        let (safeStart, safeEnd) = normalize(
            start?.value, end: end?.value, total: self.count as! Int)
        let safeStartIndex = startIndex.advancedBy(
            safeStart as! Self.Index.Distance)
        let safeEndIndex = startIndex.advancedBy(
            safeEnd as! Self.Index.Distance)
        return self[safeStartIndex..<safeEndIndex]
    }

    public subscript(index: Offset) -> Self.Generator.Element {
        let (safeStart, _) = normalize(
            index.value, end: 0, total: self.count as! Int)
        return self[startIndex.advancedBy(safeStart as! Self.Index.Distance)]
    }
}

let s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

            // equavelent in Swift Python Result
s[%+2] // s[s.startIndex.advancedBy(2)] s[2] 2
s[%-2] // s[s.endIndex.advancedBy(-2)] s[-2] 8
s[%+2, %-3] // s.dropFirst(2).dropLast(3) s[2:-2] [2, 3, 4, 5, 6]
s[%-5, nil] // s.dropFirst(s.count - 5) s[-5:] [5, 6, 7, 8, 9]
s[nil, %-8] // s.dropLast(8) s[:-8] [0, 1]


(Dave Abrahams) #2

Hi all,

A while ago there's a thread on adding Python's slicing syntax here:
http://thread.gmane.org/gmane.comp.lang.swift.evolution/124/focus=261

From it, a brief summary of the issues in Python's slicing syntax:

1. light syntax may suggest O(1) operation complexity to some.

Slicing is always O(1) in swift, so that's a desirable property.

2. negative index imposes runtime overhead
3. negative index masks fencepost errors (`foo(m-n)` supprises if you assume
   m > n)

Yes.

D's slicing syntax (https://dlang.org/d-array-article.html) was brought up as
an alternative. It does a better job in suggesting "unusual" operation (if you
treat subscripting with an index as normal). I attached a rudimentary
implementation of it at the end. It's in not way aimed at being complete or
ending up in stdlib. Hopefully, being able to play with it will generate more
discussion on this topic :slight_smile:

// (if you prefer a gist: https://gist.github.com/dduan/5c54df865fdd7b6b7548)
//
// Incomplete:
// '%' replaces the more ideal '$' (the latter can't be in name of
// operator)

You don't need it to be an operator; you can just define a var called $
and create operator overloads for + and - that let you build the
expressions you want.

// where a open ended range is in the design, `nil` is used its place
// force casting between Index.Distance and 'Int'
// only on CollectionType, but a version for string is not hard to add

Why not just make

    x[..<$-2]

work? You can create unary overloads for ..<

···

on Sat Feb 20 2016, Daniel Duan <swift-evolution@swift.org> wrote:

public struct Offset { let value: Int }

prefix operator %+ {}
prefix operator %- {}

prefix func %+(offset: Int) -> Offset {
    return Offset(value: offset)
}
prefix func %-(offset: Int) -> Offset {
    return Offset(value: -offset)
}

internal func normalize(start: Int?, end: Int?, total: Int) -> (Int, Int) {
    var actualStart = start ?? 0
    var actualEnd = end ?? total
    actualStart = actualStart < 0 ? total + actualStart : actualStart
    actualEnd = actualEnd < 0 ? total + actualEnd : actualEnd
// uncomment the following for a much more forgiving index behavior
// actualStart = actualStart < 0 ? 0 : actualStart
// actualEnd = actualEnd < 0 ? 0 : actualEnd
// let safeEnd = min(actualEnd, total)
// return (min(actualStart, safeEnd), safeEnd)
    return (actualStart, actualEnd)
}

extension CollectionType {
    public subscript(start:Offset?, end:Offset?) -> Self.SubSequence {
        let (safeStart, safeEnd) = normalize(
            start?.value, end: end?.value, total: self.count as! Int)
        let safeStartIndex = startIndex.advancedBy(
            safeStart as! Self.Index.Distance)
        let safeEndIndex = startIndex.advancedBy(
            safeEnd as! Self.Index.Distance)
        return self[safeStartIndex..<safeEndIndex]
    }

    public subscript(index: Offset) -> Self.Generator.Element {
        let (safeStart, _) = normalize(
            index.value, end: 0, total: self.count as! Int)
        return self[startIndex.advancedBy(safeStart as! Self.Index.Distance)]
    }
}

let s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

            // equavelent in Swift Python Result
s[%+2] // s[s.startIndex.advancedBy(2)] s[2] 2
s[%-2] // s[s.endIndex.advancedBy(-2)] s[-2] 8
s[%+2, %-3] // s.dropFirst(2).dropLast(3) s[2:-2] [2, 3, 4, 5, 6]
s[%-5, nil] // s.dropFirst(s.count - 5) s[-5:] [5, 6, 7, 8, 9]
s[nil, %-8] // s.dropLast(8) s[:-8] [0, 1]

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave


(David Hart) #3

I’ve read the D documentation, I’ve used similar syntax in Ruby, and I agree that something along the same lines could be really expressive. I agree at least that the current Swift syntax for your example operations is much more verbose and surprising than should be.

···

On 21 Feb 2016, at 01:41, Daniel Duan via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

A while ago there's a thread on adding Python's slicing syntax here:
http://thread.gmane.org/gmane.comp.lang.swift.evolution/124/focus=261

From it, a brief summary of the issues in Python's slicing syntax:

1. light syntax may suggest O(1) operation complexity to some.
2. negative index imposes runtime overhead
3. negative index masks fencepost errors (`foo(m-n)` supprises if you assume
  m > n)

D's slicing syntax (https://dlang.org/d-array-article.html) was brought up as
an alternative. It does a better job in suggesting "unusual" operation (if you
treat subscripting with an index as normal). I attached a rudimentary
implementation of it at the end. It's in not way aimed at being complete or
ending up in stdlib. Hopefully, being able to play with it will generate more
discussion on this topic :slight_smile:

// (if you prefer a gist: https://gist.github.com/dduan/5c54df865fdd7b6b7548)
//
// Incomplete:
// '%' replaces the more ideal '$' (the latter can't be in name of operator)
// where a open ended range is in the design, `nil` is used its place
// force casting between Index.Distance and 'Int'
// only on CollectionType, but a version for string is not hard to add

public struct Offset { let value: Int }

prefix operator %+ {}
prefix operator %- {}

prefix func %+(offset: Int) -> Offset {
   return Offset(value: offset)
}
prefix func %-(offset: Int) -> Offset {
   return Offset(value: -offset)
}

internal func normalize(start: Int?, end: Int?, total: Int) -> (Int, Int) {
   var actualStart = start ?? 0
   var actualEnd = end ?? total
   actualStart = actualStart < 0 ? total + actualStart : actualStart
   actualEnd = actualEnd < 0 ? total + actualEnd : actualEnd
// uncomment the following for a much more forgiving index behavior
// actualStart = actualStart < 0 ? 0 : actualStart
// actualEnd = actualEnd < 0 ? 0 : actualEnd
// let safeEnd = min(actualEnd, total)
// return (min(actualStart, safeEnd), safeEnd)
   return (actualStart, actualEnd)
}

extension CollectionType {
   public subscript(start:Offset?, end:Offset?) -> Self.SubSequence {
       let (safeStart, safeEnd) = normalize(
           start?.value, end: end?.value, total: self.count as! Int)
       let safeStartIndex = startIndex.advancedBy(
           safeStart as! Self.Index.Distance)
       let safeEndIndex = startIndex.advancedBy(
           safeEnd as! Self.Index.Distance)
       return self[safeStartIndex..<safeEndIndex]
   }

   public subscript(index: Offset) -> Self.Generator.Element {
       let (safeStart, _) = normalize(
           index.value, end: 0, total: self.count as! Int)
       return self[startIndex.advancedBy(safeStart as! Self.Index.Distance)]
   }
}

let s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

           // equavelent in Swift Python Result
s[%+2] // s[s.startIndex.advancedBy(2)] s[2] 2
s[%-2] // s[s.endIndex.advancedBy(-2)] s[-2] 8
s[%+2, %-3] // s.dropFirst(2).dropLast(3) s[2:-2] [2, 3, 4, 5, 6]
s[%-5, nil] // s.dropFirst(s.count - 5) s[-5:] [5, 6, 7, 8, 9]
s[nil, %-8] // s.dropLast(8) s[:-8] [0, 1]

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Daniel Duan) #4

Dave Abrahams via swift-evolution <swift-evolution@...> writes:

Why not just make

    x[..<$-2]

work? You can create unary overloads for ..<

Hi Dave, I took another stab at it today. The code is in the following link as
well as end of this email:
https://gist.github.com/dduan/fcd33a09fae4640fc83c

This implementation is almost good IMO :slight_smile:

struct Dollar {}

let $ = Dollar()

struct BInt: IntegerLiteralConvertible {
    let bool: Bool
    let int: UInt
    func relativeTo(total: Int) -> Int {
        return bool ? total - Int(int) : Int(int)
    }
    init(_ b: Bool, _ n: UInt) {
        bool = b
        int = n
    }
    init(integerLiteral value: Int) {
        bool = false
        int = UInt(value)
    }
}

func -(h: Dollar, d: UInt) -> BInt { return BInt(true, d) }

typealias IncompleteRange = (BInt?, BInt?)

prefix operator ..< {}
prefix func ..<(end: BInt) -> IncompleteRange {
    return (nil, end)
}

postfix operator ..< {}
postfix func ..<(start: BInt) -> IncompleteRange {
    return (start, nil)
}
prefix operator ... {}
prefix func ...(end: BInt) -> IncompleteRange {
    return (nil, BInt(end.bool, end.int+1))
}
postfix operator ... {}
postfix func ...(start: BInt) -> IncompleteRange {
    return (start, nil)
}

func ..<(start: BInt, end: BInt) -> IncompleteRange {
    return (start, end)
}

func ...(start: BInt, end: BInt) -> IncompleteRange {
    return (start, BInt(end.bool, end.int+1))
}

internal func normalize(range: IncompleteRange, total: Int) -> (Int, Int) {
    var actualStart = range.0 == nil ? 0 : range.0!.relativeTo(total)
    var actualEnd = range.1 == nil ? total : range.1!.relativeTo(total)
    actualStart = actualStart < 0 ? total + actualStart : actualStart
    actualEnd = actualEnd < 0 ? total + actualEnd : actualEnd
    return (actualStart, actualEnd)
}

extension CollectionType {
    subscript(halfRange: IncompleteRange) -> Self.SubSequence {
        let (safeStart, safeEnd) = normalize(
            halfRange,
            total: self.count as! Int
        )
        let safeStartIndex = startIndex.advancedBy(
            safeStart as! Self.Index.Distance)
        let safeEndIndex = startIndex.advancedBy(
            safeEnd as! Self.Index.Distance)
        return self[safeStartIndex..<safeEndIndex]
    }
}

let s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// all of these expression results in [6, 7, 8, 9]
s[6...]
s[($-4)...]
s[6..<s.count]
s[6..<$-0]
s[$-4...9]

···

on Sat Feb 20 2016, Daniel Duan <swift-evolution <at> swift.org> wrote:


(Dave Abrahams) #5

Dave Abrahams via swift-evolution <swift-evolution@...> writes:

Why not just make

    x[..<$-2]

work? You can create unary overloads for ..<

Hi Dave, I took another stab at it today. The code is in the following link as
well as end of this email:
https://gist.github.com/dduan/fcd33a09fae4640fc83c

This implementation is almost good IMO :slight_smile:

What's the case that keeps it from actually being good?

···

on Sat Feb 27 2016, Daniel Duan <swift-evolution@swift.org> wrote:

on Sat Feb 20 2016, Daniel Duan <swift-evolution <at> swift.org> wrote:

struct Dollar {}

let $ = Dollar()

struct BInt: IntegerLiteralConvertible {
    let bool: Bool
    let int: UInt
    func relativeTo(total: Int) -> Int {
        return bool ? total - Int(int) : Int(int)
    }
    init(_ b: Bool, _ n: UInt) {
        bool = b
        int = n
    }
    init(integerLiteral value: Int) {
        bool = false
        int = UInt(value)
    }
}

func -(h: Dollar, d: UInt) -> BInt { return BInt(true, d) }

typealias IncompleteRange = (BInt?, BInt?)

prefix operator ..< {}
prefix func ..<(end: BInt) -> IncompleteRange {
    return (nil, end)
}

postfix operator ..< {}
postfix func ..<(start: BInt) -> IncompleteRange {
    return (start, nil)
}
prefix operator ... {}
prefix func ...(end: BInt) -> IncompleteRange {
    return (nil, BInt(end.bool, end.int+1))
}
postfix operator ... {}
postfix func ...(start: BInt) -> IncompleteRange {
    return (start, nil)
}

func ..<(start: BInt, end: BInt) -> IncompleteRange {
    return (start, end)
}

func ...(start: BInt, end: BInt) -> IncompleteRange {
    return (start, BInt(end.bool, end.int+1))
}

internal func normalize(range: IncompleteRange, total: Int) -> (Int, Int) {
    var actualStart = range.0 == nil ? 0 : range.0!.relativeTo(total)
    var actualEnd = range.1 == nil ? total : range.1!.relativeTo(total)
    actualStart = actualStart < 0 ? total + actualStart : actualStart
    actualEnd = actualEnd < 0 ? total + actualEnd : actualEnd
    return (actualStart, actualEnd)
}

extension CollectionType {
    subscript(halfRange: IncompleteRange) -> Self.SubSequence {
        let (safeStart, safeEnd) = normalize(
            halfRange,
            total: self.count as! Int
        )
        let safeStartIndex = startIndex.advancedBy(
            safeStart as! Self.Index.Distance)
        let safeEndIndex = startIndex.advancedBy(
            safeEnd as! Self.Index.Distance)
        return self[safeStartIndex..<safeEndIndex]
    }
}

let s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// all of these expression results in [6, 7, 8, 9]
s[6...]
s[($-4)...]
s[6..<s.count]
s[6..<$-0]
s[$-4...9]

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave