[swift-evolution] [Proposal] Random Unification

Jonathan Hull jhull at gbis.com
Thu Nov 30 18:40:25 CST 2017


> On Nov 30, 2017, at 4:11 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
> 
> On Thu, Nov 30, 2017 at 5:29 PM, Jonathan Hull <jhull at gbis.com <mailto:jhull at gbis.com>> wrote:
> 
>> On Nov 30, 2017, at 2:30 PM, Xiaodi Wu <xiaodi.wu at gmail.com <mailto:xiaodi.wu at gmail.com>> wrote:
>> 
>> On Thu, Nov 30, 2017 at 3:58 PM, Dave DeLong via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>> 
>> 
>>> On Nov 30, 2017, at 2:48 PM, Jonathan Hull via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>> 
>>> I would personally go with:
>>> 
>>> 	Int.random //Returns a random Int
>> 
>> “Type.random” is so rarely used as to not be worth the addition, IMO. If you really need a random element from the *entire* domain, then I think you should have to manually create the ClosedRange<T> yourself.
>> 
>>> 	Int.random(in: ClosedRange<Int>) //Works for Comparable types. Gives a result from the closed range. Closed Range is never empty.
>> 
>> This is redundant. In order to pick a random element, you’re saying I should have to do “Int.random(0 ..< 10)”? The redundancy here is that I have to specify Int twice: once for the “.random” call, and again for the type of the range. We can do better than that.
>> 
>>> 	[0,2,3].randomElement //Returns a random element from the collection
>> 
>> I strongly believe this should be a method, not a property. Properties, like .first and .last, are expected to return the same value each time you access them. “.random” inherently breaks that.
>>  
>> FWIW--and this isn't a vote, I know--I largely agree with Dave DeLong's conclusions above, and for substantially the same reasons.
>>> 
>>> Then a version of each with a ‘using:’ parameter which takes a generator/source:
>>> 
>>> 	Int.random(using: RandomSource) //Returns a random Int using the given source of randomness
>>> 	Int.random(in: ClosedRange<Int>, using: RandomSource)
>>> 	[0,2,3].randomElement(using: RandomSource)
>>> 
>>> In my own RandomSource & RandomSourceCreatable protocols, I frequently use random colors and sizes as well.  The issue there is that you really want a closed range for each dimension. I wish Swift had a better notion of dimensionality baked into the language. 
>>> 
>>> What I ended up doing was having a “constraints” parameter which took an array of constraints which corresponded to various dimensions.  It works for me, but it might be a bit complex for something in the standard library.
>>> 
>>> Honestly, given the current capabilities of Swift what this really calls for is custom initializers/functions for dimensional types:
>>> 
>>> 	UIColor.random //This comes from the protocol
>>> 	UIColor.random(hue: ClosedRange<CGFloat> = 0…1, saturation: ClosedRange<CGFloat> = 0…1, brightness: ClosedRange<CGFloat> = 0…1, alpha: ClosedRange<CGFloat> = 1…1)
>>> 	//…and of course the same as above, but with ‘using:'
>>> 
>>> Then you can easily get random colors which look like they belong together:
>>> 	
>>> 	let myColor = UIColor.random(saturation: 0.2…0.2, brightness: 0.6…0.6) 
>>> 
>>> There would probably also be a convenience version taking CGFloats and passing them to the real function as ranges:
>>> 
>>> 	let myColor = UIColor.random(saturation: 0.2, brightness: 0.6)
>>> 
>>> 
>>> This means that our default RandomSource needs to be publicly available, so that the custom functions can use it as the default…
>> 
>> 
>> It does not. Having actually implemented some version of these APIs, it's readily apparent now to me that all custom types can simply call Int.random(in:) (or UnsafeRawBufferPointer<T>.random(byteCount:), or whatever else we want to have in the standard library) to get random values from the default RNG for any built-in type and size. The actual default random need never be exposed publicly, and since its functions are strictly redundant to these other APIs (which, of course, are the "currency" APIs that our purpose here is to design and make public), the default random is required only for internal implementation of the "currency" APIs and (a) is better off *not* exposed; (b) doesn't need to be of the same type as other RNGs, conform to the same protocols, or for that matter, does not even need to be a type or be written in Swift.
> 
> I have also implemented some version of these APIs, both for Random and RepeatablyRandom sources.
> 
> I get what you are saying about just being able to use the constructs from Int, etc…, but we still need a public default.  Let me give a concrete example of CGSize.  Yes, we want to just use CGFloat’s random, but if we don’t have a publicly available way to call the default then we have to implement the same algorithm twice, which is problematic. (Code written in Mail)
> 
> static func random(width widthRange: ClosedRange<CGFloat>, height heightRange: ClosedRange<CGFloat>, using source: RandomSource = .default) -> CGSize {
> 	let w = CGFloat.random(in: widthRange, using: source)
> 	let h = CGFloat.random(in: heightRange, using: source)
> 	return CGSize(width: w, height: h)
> }
> 
> Without the default I would have to have a second version which used CGFloat(in:) in order to use the default source/generator, which means I would have to update both places when I make changes.  Much better to just allow a default value for ‘using:'.
> 
> Ah, this is assuming that one thinks it is a good idea to have `using:` variants. Firstly, I'm not entirely sold on them. But let's suppose you've convinced me. The example above illustrates a huge footgun:
> 
> Why do people want to use a different RNG?

I *need* to be able to insert a repeatably random source, otherwise this whole exercise is useless/detrimental to me.  My use case is that I need repeatably random values for various graphics effects, as well as to seed various content sources.  If the source isn’t random, then things will change every time the user resizes something on the screen.

For example, if I want a bezier curve to look hand-drawn, I break it into a number of smaller curves, and then offset the points by a small amount (by generating bounded random CGVectors). When the source is repeatable, this looks awesome. If the source is not repeatable, it will dance around whenever the user moves it. It feels flaky.

It is much more important when it comes to generated content. There without a repeatable source, the content will change every time the user moves/resizes it (or even reloads the file from a save).

You will find that there are a number of cases where games need this too...

> A very common reason: to obtain a different distribution of random values. Now, can you simply perform memberwise initialization as you show above and obtain a final instance of the desired distribution simply because the stored properties are drawn from that distribution? No no no no no! This implementation is incorrect.

I’m not following you here.

> Not only is it not _problematic_ to require two distinct implementations, it's almost certainly required if you want to have any hope of implementing them correctly.

I’m still not following. I don’t see why you would want different initialization patterns for different sources.  The example I gave was about multi-dimensional structs/classes, so the distributions should be independent based on axis…

Thanks,
Jon




-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171130/36377296/attachment.html>


More information about the swift-evolution mailing list