Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move random(inRange:) to Collection extension #97

Closed
wants to merge 5 commits into from

Conversation

andrewdolce
Copy link

@andrewdolce andrewdolce commented Nov 22, 2016

Addresses issue #89.

This was a bit more complex than originally expected. The function is no longer defined on CountableClosedRange<Int> specifically, but applies to any range with Bound conforming to the SignedInteger protocol. The arithmetic also had to be altered slightly to account for the generic bound/stride type.

Updates after refactor:

Revised replacement for ip_random(inRange:)

The ip_random function now lives on an extension of Collection. This means we can still use it to return a random value within a numerical range (since ranges are collections) as well as in many other situations, like say grabbing a random element from an array.

The new unit tests remain the same as the first version of this PR: I introduced a way to provide an alternative RNG function instead of arc4random_uniform. This was done entirely for testing purposes, and I've left it out of the public API. See the new unit test code and comments for a longer explanation. I'm really interested in feedback as to whether others agree with the testing approach.

Replacement of ip_random() on UnsignedInteger

There is an additional ip_random function that can be called on any class conforming to UnsignedInteger, to return a random value in range [0, max]. A private library [name redacted] uses this to generate random values for UInt8. However the implementation is flawed in that it relies on arc4random_uniform which expects UInt32. So if the function is called on a larger type, like UInt64 (or UInt on a 64-bit device), the result is an overflow crash.

The easiest way to fix this without seeking out a new implementation is to restrict this function for use on UInt32 and under, which I've done by introducing a protocol adopted specifically by UInt8, UInt16, and UInt32.

Copy link
Contributor

@brightredchilli brightredchilli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the change to signed integer.


import Foundation

public extension CountableClosedRange where Bound: SignedInteger {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question here, does this impose any limitations on the stride then? can there be countableclosedrange of stride 2? from 0...4?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS this is really nice.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It hasn't been tested with different strides. I will address that before merging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if there can indeed be different strides, then this is kind of worrying...we then need to make sure that the random output falls on the stride boundary, right? am I thinking about this right? 0...4 with stride 2 only had 0,2,4 as valid values.

})

guard let rngUpperBound = capturedRNGUpperBound, rngUpperBound > 0 else {
XCTAssert(false, "Expected that RNG function upper bound \(capturedRNGUpperBound) is non-nil and positive.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather we translated some items in the guard into a more readable assertion.

ie

XCTAssert(rngUpperBound > 0, "Upper bound is less than zero, expected more than zero")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, per guard style guide, we should not use them as control flow, but only for early return. In addition, i dont think if lets are better in tests.


for mockRNGValue in 0..<rngUpperBound {
let sample = testRange.ip_random(withUniformRNGFunction: { upperBound in
XCTAssertEqual(upperBound, rngUpperBound, "Expected RNG function to be called with a consistent upper bound each time." )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im slightly confused as to why this needs to be in a loop. mockRNGValue is not used in any assertion

Copy link
Author

@andrewdolce andrewdolce Nov 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am returning mockRNGValue as the output of the mock RNG function. It's a stand-in for a possible value that arc4random would output. In other words, it's an input for ip_random.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, what im saying is that calling it in a loop doesn't actually test anything different.


class CountableClosedRangeExtensionTests: XCTestCase {

// This test is complex. Here is an explanation as to why:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totallly agree with the reasoning outlined in these comments, but I think we need to pare it down a little :D

@@ -0,0 +1,25 @@
//
// CountableClosedRange+Extensions.swift
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like Ranges deserve their own home under StandardLibrary. Thoughts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

@andrewdolce
Copy link
Author

@brightredchilli So in exploring the variable stride issue, I've come to the realization that (I think) ip_random could live inside an extension on Collection instead. Then we'd get random element access for all collections, including CountableClosedRange. Working on that now.

@brightredchilli
Copy link
Contributor

@andrewdolce - it sounds like there is more work to be done there, maybe we can open an issue for that and get these guys checked in?

@ghost
Copy link

ghost commented Dec 9, 2016

1 Message
📖 Executed 95 tests, with 0 failures (0 unexpected) in 0.314 (0.395) seconds

Generated by 🚫 danger

@andrewdolce andrewdolce self-assigned this Feb 14, 2017
@andrewdolce andrewdolce changed the title Move random(inRange:) to CountableClosedRange extension Move random(inRange:) to Collection extension Feb 23, 2017
@andrewdolce
Copy link
Author

@brightredchilli @paulrolfe I updated this finally. Could you guys give it another look? I'd like Paul to weigh in on the UnsignedInteger extension change, since it potentially impacts BMAP. Thanks!

Copy link
Contributor

@brightredchilli brightredchilli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would advocate for not testing, but otherwise this is a good to go for me.


class RandomizableUnsignedIntegerTests: XCTestCase {
func testRandom() {
let _: UInt8 = UInt8.ip_random()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why let _? isn't _ = UInt8.ip_random() enough?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, I'll fix.


public extension Collection {
public func ip_random() -> Self.Iterator.Element {
return ip_random(withUniformRNGFunction: arc4random_uniform)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we ever change this? if not can we just combine this and the below function?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove this. The only purpose of it was to let the tests specify a mock RNG function.


class CollectionRandomTests: XCTestCase {

// This test is complex. Here is an explanation as to why:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think these tests are quite comprehensive but for the reasons that you have mentioned, i kind of think that we should not include them...they seem unwieldy to update and seem to be dancing around the 'actual' random test.

Fwiw, I ran this code:

var array = repeatElement(0, count: Int(UInt8.max)).ip_toArray()

        for _ in 0...1000000 {
            let index = UInt8.ip_random()
            array[Int(index)] = array[Int(index)] + 1
        }
        print(array)

and got

[3927, 3857, 3833, 3904, 3933, 3932, 3956, 3889, 4012, 3949, 3924, 3888, 3871, 3890, 3891, 3900, 4003, 3945, 3804, 3898, 3934, 4019, 3976, 4001, 3885, 3893, 3940, 4007, 3872, 4012, 3954, 3971, 3829, 4060, 3953, 3850, 4010, 3873, 3963, 3979, 4003, 4006, 3922, 3836, 3893, 3958, 3799, 3873, 3894, 3976, 3840, 3890, 3964, 3916, 3917, 3978, 3918, 3816, 3925, 3880, 3974, 3914, 4006, 3945, 3748, 3888, 4020, 3876, 3981, 3994, 3941, 3809, 3962, 3929, 3922, 3854, 3928, 3940, 3978, 3930, 3912, 3864, 3818, 3983, 3895, 3869, 3859, 3972, 3972, 3905, 3948, 3992, 4049, 3936, 3914, 3895, 3931, 3985, 3882, 3895, 3926, 4041, 3865, 4000, 3965, 3987, 3853, 3987, 4011, 3823, 3815, 3902, 3968, 3940, 3818, 3866, 4001, 3974, 3866, 3980, 3952, 4053, 3890, 4031, 3896, 3843, 3857, 3893, 3905, 3967, 4091, 4035, 3918, 3881, 3826, 3955, 3898, 3896, 3854, 3966, 3854, 4006, 4012, 3878, 3957, 3914, 3922, 3934, 3885, 4037, 3962, 3937, 3848, 3920, 3834, 3860, 3859, 3858, 3966, 3900, 3948, 3929, 3856, 3925, 3824, 3888, 3934, 3848, 3980, 3935, 3942, 3923, 3934, 3871, 3886, 3867, 3886, 3904, 3886, 3960, 3848, 3890, 3871, 4010, 3846, 3962, 3919, 3897, 3884, 3897, 3953, 3901, 3826, 3899, 4008, 3965, 4018, 3999, 3870, 3938, 3853, 3887, 3948, 3889, 3860, 3951, 3920, 3882, 3916, 3913, 3875, 4023, 3944, 3956, 3851, 3921, 3926, 4018, 3916, 3955, 3910, 3842, 3908, 3915, 3935, 3899, 4005, 3975, 3947, 3884, 3869, 3983, 3831, 3931, 3926, 3897, 3927, 3980, 3937, 3953, 3927, 3889, 3849, 3981, 3929, 3929, 3943, 3872, 3993, 3902, 3837, 4080, 3837, 3894, 3810]

Not by any means a scientific test, but I feel good about it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point about maintainability. I'll take them out and leave in the original tests.

@cprime cprime closed this Feb 6, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants