How to be DRY on ranges and closed ranges?


(Jean-Denis Muys) #1

Hi,

I defined this:

func random(from r: Range<Int>) -> Int {
    let from = r.lowerBound
    let to = r.upperBound
    
    let rnd = arc4random_uniform(UInt32(to-from))
    return from + Int(rnd)
}

so that I can do:

let testRandomValue = random(from: 4..<8)

But this will not let me do:

let otherTestRandomValue = random(from: 4...10)

The error message is a bit cryptic:

“No ‘…’ candidate produce the expected contextual result type ‘Range<Int>’”

What is happening is that 4…10 is not a Range, but a ClosedRange.

Of course I can overload my function above to add a version that takes a ClosedRange.

But this is not very DRY.

What would be a more idiomatic way?

Thanks,

Jean-Denis


(David Hart) #2

I’ve been bitten by that quite a few times. I’m not a fan of the new distinction between Range and ClosedRange. I understand the reasoning behind them, but the new model is creating more problems for me than the it solves.

David.

···

On 12 Oct 2016, at 12:21, Jean-Denis Muys via swift-users <swift-users@swift.org> wrote:

Hi,

I defined this:

func random(from r: Range<Int>) -> Int {
    let from = r.lowerBound
    let to = r.upperBound
    
    let rnd = arc4random_uniform(UInt32(to-from))
    return from + Int(rnd)
}

so that I can do:

let testRandomValue = random(from: 4..<8)

But this will not let me do:

let otherTestRandomValue = random(from: 4...10)

The error message is a bit cryptic:

“No ‘…’ candidate produce the expected contextual result type ‘Range<Int>’”

What is happening is that 4…10 is not a Range, but a ClosedRange.

Of course I can overload my function above to add a version that takes a ClosedRange.

But this is not very DRY.

What would be a more idiomatic way?

Thanks,

Jean-Denis

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


(Adriano Ferreira) #3

Hi there!

Ole Begeman offers here <https://oleb.net/blog/2016/09/swift-3-ranges/> (take a look at the bottom of the page) an interesting consideration about converting between half-open and closed ranges.

As of now, it seems the way to go is by overloading…

import Foundation

func random(from range: Range<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound)))
}

func random(from range: ClosedRange<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound + 1)))
}

let r1 = random(from: 4 ..< 8)
let r2 = random(from: 6 ... 8)

Cheers,

— A

···

On Oct 12, 2016, at 6:21 AM, Jean-Denis Muys via swift-users <swift-users@swift.org> wrote:

Hi,

I defined this:

func random(from r: Range<Int>) -> Int {
    let from = r.lowerBound
    let to = r.upperBound
    
    let rnd = arc4random_uniform(UInt32(to-from))
    return from + Int(rnd)
}

so that I can do:

let testRandomValue = random(from: 4..<8)

But this will not let me do:

let otherTestRandomValue = random(from: 4...10)

The error message is a bit cryptic:

“No ‘…’ candidate produce the expected contextual result type ‘Range<Int>’”

What is happening is that 4…10 is not a Range, but a ClosedRange.

Of course I can overload my function above to add a version that takes a ClosedRange.

But this is not very DRY.

What would be a more idiomatic way?

Thanks,

Jean-Denis

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


(Hooman Mehr) #4

The more idiomatic way is to look at API design in a new way. Note these points:

1. `Countable` variant is preferred when you want to deal with integer ranges as it more closely matches the element type.
2. Both countable range variants share a common protocol conformance already: `RandomAccessCollection`
3. Swift API design prefers member functions to free functions.

Hence a more idiomatic (Swifty) API would probably be something like this:

extension RandomAccessCollection {
    
    func random() -> Iterator.Element? {
        
        guard count > 0 else { return nil }
        
        let offset = arc4random_uniform(numericCast(count))
        
        let i = index(startIndex, offsetBy: numericCast(offset))
        
        return self[i]
    }
}

Using the above, both cases work and there is no repetition:

(4..<10).random()
(4...9).random()

It also makes a lot more possible:

let people = ["David", "Chris", "Joe", "Jordan", "Tony"]
let winner = people.random()

···

On Oct 12, 2016, at 3:21 AM, Jean-Denis Muys via swift-users <swift-users@swift.org> wrote:

But this is not very DRY.

What would be a more idiomatic way?


(Dave Abrahams) #5

In our original design we had a common protocol to which open and closed
ranges conformed, and you could pass an instance of RangeProtocol. That
was removed because the overall complexity was not a win, but I predict
it will come back for Swift 4:
https://github.com/apple/swift/pull/3737

But for what you want to do, you don't need anything that complicated:

protocol CompleteRange {
  associatedtype Bound : Comparable
  var lowerBound : Bound { get }
  var upperBound : Bound { get }
}

extension CountableRange : CompleteRange {}
extension CountableClosedRange : CompleteRange {}

import Darwin
func random<R: CompleteRange>(from r: R) -> Int where R.Bound == Int, R: Collection {
  let rnd = arc4random_uniform(numericCast(r.count))
  return r.lowerBound + Int(rnd)
}

print(random(from: -5..<5), random(from: -20...20))

Hope this helps,

···

on Wed Oct 12 2016, Jean-Denis Muys <swift-users-AT-swift.org> wrote:

Hi,

I defined this:

func random(from r: Range<Int>) -> Int {
    let from = r.lowerBound
    let to = r.upperBound

    let rnd = arc4random_uniform(UInt32(to-from))
    return from + Int(rnd)
}

so that I can do:

let testRandomValue = random(from: 4..<8)

But this will not let me do:

let otherTestRandomValue = random(from: 4...10)

The error message is a bit cryptic:

“No ‘…’ candidate produce the expected contextual result type ‘Range<Int>’”

What is happening is that 4…10 is not a Range, but a ClosedRange.

Of course I can overload my function above to add a version that takes a ClosedRange.

But this is not very DRY.

What would be a more idiomatic way?

--
-Dave


(Hooman Mehr) #6

I recommend having explicit precondition and reducing repetition like this:

import Foundation

func random(from range: CountableRange<Int>) -> Int {
    
    precondition(range.count > 0,
                 "The range can't be empty.")

    return random(from: CountableClosedRange(range))
}

func random(from range: CountableClosedRange<Int>) -> Int {
    
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    precondition(upperBound - lowerBound < Int(UInt32.max),
                 "The range \(range) is too wide. It shouldn't be wider than \(UInt32.max).")
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound + 1)))
}

let r1 = random(from: 4 ..< 8)
let r2 = random(from: 6 ... 8)

Once we have the new improved Integer protocols <https://github.com/apple/swift-evolution/blob/master/proposals/0104-improved-integers.md> in place, you will be able to make it generic to support all integer types. (It is possible now, but too messy to be worth doing.)

···

On Oct 12, 2016, at 1:23 PM, Adriano Ferreira via swift-users <swift-users@swift.org> wrote:

Hi there!

Ole Begeman offers here <https://oleb.net/blog/2016/09/swift-3-ranges/> (take a look at the bottom of the page) an interesting consideration about converting between half-open and closed ranges.

As of now, it seems the way to go is by overloading…

import Foundation

func random(from range: Range<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound)))
}

func random(from range: ClosedRange<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound + 1)))
}

let r1 = random(from: 4 ..< 8)
let r2 = random(from: 6 ... 8)

Cheers,

— A

On Oct 12, 2016, at 6:21 AM, Jean-Denis Muys via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Hi,

I defined this:

func random(from r: Range<Int>) -> Int {
    let from = r.lowerBound
    let to = r.upperBound
    
    let rnd = arc4random_uniform(UInt32(to-from))
    return from + Int(rnd)
}

so that I can do:

let testRandomValue = random(from: 4..<8)

But this will not let me do:

let otherTestRandomValue = random(from: 4...10)

The error message is a bit cryptic:

“No ‘…’ candidate produce the expected contextual result type ‘Range<Int>’”

What is happening is that 4…10 is not a Range, but a ClosedRange.

Of course I can overload my function above to add a version that takes a ClosedRange.

But this is not very DRY.

What would be a more idiomatic way?

Thanks,

Jean-Denis

_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

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


(Hooman Mehr) #7

I recommend having explicit precondition and reducing repetition like this:

import Foundation

func random(from range: CountableRange<Int>) -> Int {
    
    precondition(range.count > 0,
                 "The range can't be empty.")

    return random(from: CountableClosedRange(range))
}

func random(from range: CountableClosedRange<Int>) -> Int {
    
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    precondition(upperBound - lowerBound < Int(UInt32.max),
                 "The range \(range) is too wide. It shouldn't be wider than \(UInt32.max).")
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound + 1)))
}

let r1 = random(from: 4 ..< 8)
let r2 = random(from: 6 ... 8)

Once we have the new improved Integer protocols <https://github.com/apple/swift-evolution/blob/master/proposals/0104-improved-integers.md> in place, you will be able to make it generic to support all integer types. (It is possible now, but too messy to be worth doing.)

···

On Oct 12, 2016, at 1:23 PM, Adriano Ferreira via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Hi there!

Ole Begeman offers here <https://oleb.net/blog/2016/09/swift-3-ranges/> (take a look at the bottom of the page) an interesting consideration about converting between half-open and closed ranges.

As of now, it seems the way to go is by overloading…

import Foundation

func random(from range: Range<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound)))
}

func random(from range: ClosedRange<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound
    
    return lowerBound + Int(arc4random_uniform(UInt32(upperBound - lowerBound + 1)))
}

let r1 = random(from: 4 ..< 8)
let r2 = random(from: 6 ... 8)

Cheers,

— A

On Oct 12, 2016, at 6:21 AM, Jean-Denis Muys via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Hi,

I defined this:

func random(from r: Range<Int>) -> Int {
    let from = r.lowerBound
    let to = r.upperBound
    
    let rnd = arc4random_uniform(UInt32(to-from))
    return from + Int(rnd)
}

so that I can do:

let testRandomValue = random(from: 4..<8)

But this will not let me do:

let otherTestRandomValue = random(from: 4...10)

The error message is a bit cryptic:

“No ‘…’ candidate produce the expected contextual result type ‘Range<Int>’”

What is happening is that 4…10 is not a Range, but a ClosedRange.

Of course I can overload my function above to add a version that takes a ClosedRange.

But this is not very DRY.

What would be a more idiomatic way?

Thanks,

Jean-Denis

_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users


#8

You could also create a “Range” protocol with “lowerBound” and “upperBound”
properties. Conform all the range types to it, and make your function take
generic over the protocol.

Nevin

···

On Wed, Oct 12, 2016 at 7:21 PM, Hooman Mehr via swift-users < swift-users@swift.org> wrote:

I recommend having explicit precondition and reducing repetition like this:

import Foundation

func random(from range: CountableRange<Int>) -> Int {

    precondition(range.count > 0,
                 "The range can't be empty.")

    return random(from: CountableClosedRange(range))
}

func random(from range: CountableClosedRange<Int>) -> Int {

    let lowerBound = range.lowerBound
    let upperBound = range.upperBound

    precondition(upperBound - lowerBound < Int(UInt32.max),
                 "The range \(range) is too wide. It shouldn't be wider
than \(UInt32.max).")

    return lowerBound + Int(arc4random_uniform(UInt32(upperBound -
lowerBound + 1)))
}

let r1 = random(from: 4 ..< 8)
let r2 = random(from: 6 ... 8)

Once we have the new improved Integer protocols
<https://github.com/apple/swift-evolution/blob/master/proposals/0104-improved-integers.md> in
place, you will be able to make it generic to support all integer types.
(It is possible now, but too messy to be worth doing.)

On Oct 12, 2016, at 1:23 PM, Adriano Ferreira via swift-users < > swift-users@swift.org> wrote:

Hi there!

Ole Begeman offers here <https://oleb.net/blog/2016/09/swift-3-ranges/> (take
a look at the bottom of the page) an interesting consideration about
converting between half-open and closed ranges.

As of now, it seems the way to go is by overloading…

import Foundation

func random(from range: Range<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound

    return lowerBound + Int(arc4random_uniform(UInt32(upperBound -
lowerBound)))
}

func random(from range: ClosedRange<Int>) -> Int {
    let lowerBound = range.lowerBound
    let upperBound = range.upperBound

    return lowerBound + Int(arc4random_uniform(UInt32(upperBound -
lowerBound + 1)))
}

let r1 = random(from: 4 ..< 8)
let r2 = random(from: 6 ... 8)

Cheers,

— A

On Oct 12, 2016, at 6:21 AM, Jean-Denis Muys via swift-users < > swift-users@swift.org> wrote:

Hi,

I defined this:

func random(from r: Range<Int>) -> Int {
    let from = r.lowerBound
    let to = r.upperBound

    let rnd = arc4random_uniform(UInt32(to-from))
    return from + Int(rnd)
}

so that I can do:

let testRandomValue = random(from: 4..<8)

But this will not let me do:

let otherTestRandomValue = random(from: 4...10)

The error message is a bit cryptic:

“No ‘…’ candidate produce the expected contextual result type ‘Range<Int>’”

What is happening is that 4…10 is not a Range, but a ClosedRange.

Of course I can overload my function above to add a version that takes a
ClosedRange.

But this is not very DRY.

What would be a more idiomatic way?

Thanks,

Jean-Denis

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

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

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


(Hooman Mehr) #9

On more thing:

The result type of my `random()` extension is optional. This might not be ideal. It is not an expected and likely case that `random()` returns nil. I would personally prefer it to be non-optional and crash if the array is empty, just like it would if you tried to reference a non-existing index in the collection. So, the version I actually prefer is:

extension RandomAccessCollection {
    
    func random() -> Iterator.Element {
        
        precondition(count > 0, "There is no element to random pick.")
        
        let offset = arc4random_uniform(numericCast(count))
        
        let i = index(startIndex, offsetBy: numericCast(offset))
        
        return self[i]
    }
}

···

On Oct 13, 2016, at 4:08 AM, Jean-Denis Muys <jdmuys@gmail.com> wrote:

Thank you for this Hooman, I think this is what I was looking for.

JD

On Thu, Oct 13, 2016 at 4:26 AM, Hooman Mehr <hooman@mac.com <mailto:hooman@mac.com>> wrote:

On Oct 12, 2016, at 3:21 AM, Jean-Denis Muys via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

But this is not very DRY.

What would be a more idiomatic way?

The more idiomatic way is to look at API design in a new way. Note these points:

1. `Countable` variant is preferred when you want to deal with integer ranges as it more closely matches the element type.
2. Both countable range variants share a common protocol conformance already: `RandomAccessCollection`
3. Swift API design prefers member functions to free functions.

Hence a more idiomatic (Swifty) API would probably be something like this:

extension RandomAccessCollection {
    
    func random() -> Iterator.Element? {
        
        guard count > 0 else { return nil }
        
        let offset = arc4random_uniform(numericCast(count))
        
        let i = index(startIndex, offsetBy: numericCast(offset))
        
        return self[i]
    }
}

Using the above, both cases work and there is no repetition:

(4..<10).random()
(4...9).random()

It also makes a lot more possible:

let people = ["David", "Chris", "Joe", "Jordan", "Tony"]
let winner = people.random()


(Ole Begemann) #10

The more idiomatic way is to look at API design in a new way. Note these
points:

1. `Countable` variant is preferred when you want to deal with integer
ranges as it more closely matches the element type.
2. Both countable range variants share a common protocol conformance
already: `RandomAccessCollection`
3. Swift API design prefers member functions to free functions.

Hence a more idiomatic (Swifty) API would probably be something like this:

Thanks for sharing this solution, Hooman, I really like it. I think you're making a great point about considering the appropriate abstraction for an algorithm, which is why I blogged about this: https://oleb.net/blog/2016/10/generic-range-algorithms/

I hope you don't mind.

Ole


(Dave Abrahams) #11

Hey, that is pretty awesome!

···

on Wed Oct 12 2016, Hooman Mehr <swift-users-AT-swift.org> wrote:

On Oct 12, 2016, at 3:21 AM, Jean-Denis Muys via swift-users <swift-users@swift.org> wrote:

But this is not very DRY.

What would be a more idiomatic way?

The more idiomatic way is to look at API design in a new way. Note these points:

1. `Countable` variant is preferred when you want to deal with integer ranges as it more closely
matches the element type.
2. Both countable range variants share a common protocol conformance already:
`RandomAccessCollection`
3. Swift API design prefers member functions to free functions.

Hence a more idiomatic (Swifty) API would probably be something like this:

extension RandomAccessCollection {

    func random() -> Iterator.Element? {

        guard count > 0 else { return nil }

        let offset = arc4random_uniform(numericCast(count))

        let i = index(startIndex, offsetBy: numericCast(offset))

        return self[i]
    }
}

Using the above, both cases work and there is no repetition:

(4..<10).random()
(4...9).random()

It also makes a lot more possible:

let people = ["David", "Chris", "Joe", "Jordan", "Tony"]
let winner = people.random()

--
-Dave