> On Jan 10, 2018, at 4:42 PM, Nate Cook <natec...@apple.com> wrote:
>> Right. I guess my thought is that I would like them to be able to use a 
>> standard creation pattern so it doesn’t vary from type to type (that is the 
>> whole point of “unification” in my mind).  In my own code, I have a concept 
>> of constraint, of which a set are passed to the object being created. This 
>> allows me to random create colors which look good together, etc….  I then 
>> have some convenience methods which just automatically create an appropriate 
>> constraint from a range where appropriate.  I’d really like to see something 
>> standard which allows for constraints other than simple ranges.
> 
> Is it possible for you to share some of this code?

Sure. Small disclaimer that this was originally written back in the Swift 1~2 
days, so it is overdue for a simplifying rewrite.

Also, I should point out that the term “Source” has a special meaning in my 
code.  It basically means that something will provide an ~infinite collection 
of values of a type T.   I have what I call a “ConstantSource” which just wraps 
a T and gives it back when asked.  But then I have a bunch of other “sources" 
which let you create repeating patterns and do deferred calculations and things 
like that.  Finally I have a “RandomSource” which is part of what started this 
discussion.  You set up a RandomSource with a set of constraints, and then it 
gives you random values of T that adhere to those constraints (e.g. colors with 
a range of hues but the same saturation) whenever you ask for them.

This is really useful for doing things like graphic effects because, for 
example, I can ask for a source of colors and a source of line widths and then 
get out a large variety of interesting patterns from the same algorithm.  I can 
make simple stripes with ConstantSources, or I can make repeating patterns of 
lines with repeating sources, or I can have random colors which look good 
together by using a RandomSource.  I can take a BezierPath and make it look 
hand-drawn by breaking it into a bunch of lines and then offset the points a 
small amount using a RandomSource of CGVectors.

Not sure how useful this concept of randomness (and pattern) is to others, but 
I find it immensely useful!  Not sure of the best way to implement it.  The way 
I do it is a type erased protocol with private conforming structs and then 
public initializers on the type-erasing box.  The end result is that I can just 
say:

        let myConst = Source(1) //ConstantSource with 1 as a value
        let myPattern = Source([1, 2]) //OrderedSource which repeats 1, then 2 
over and over forever
        let myMeta = Source([myConst, myPattern]) //Will alternate between 
sub-sources in order. Can be nested.
        //…and so on.

It is quite extensible and can make very complex/interesting patterns very 
easily.  What I like about it is that (well controlled) random values and 
patterns or constant values can be interchanged very easily.

The RandomSource has a RandomSourceCreatable Protocol that lets it take random 
bits and turn them into objects/structs of T adhering to the given constraints. 
 This is way more complex under the hood than it needs to be, but it works well 
in practice, and I haven’t gotten around to cleaning it up yet:

        public protocol RandomSourceCreatable {
        associatedtype ConstraintType = Self
    
        ///This should be implimented by simple types without internal 
components
        static func createRandom(rnd value:RandomSourceValue, 
constraint:RandomSourceConstraint<ConstraintType>)->Self
    
        ///This should be implimented by complex types with multiple axis of 
constraints
        static func createRandom(rnd value:RandomSourceValue, 
constraints:[String:RandomSourceConstraint<ConstraintType>])->Self
    
        ///Returns the proper dimension for the type given the constraints
        static func dimension(given 
contraints:[String:RandomSourceConstraint<ConstraintType>])->RandomSourceDimension
    
        ///Validates the given contraints to make sure they can create valid 
objects. Only needs to be overridden for extremely complex types
        static func validateConstraints(_ 
constraints:[String:RandomSourceConstraint<ConstraintType>])->Bool
    
        ///Convienience method which provides whitelist of keys for implicit 
validation of constraints
        static var allowedConstraintKeys:Set<String> {get}
   }

Most of these things also have default implementations so you only really have 
to deal with them for complex cases like colors or points.  The constraints are 
given using a dictionary with string keys and a RandomSourceConstraint value, 
which is defined like this:

        public enum RandomSourceConstraint<T> {
        case none
        case constant(T)
        case min(T)
        case max(T)
        case range (T,T)
        case custom ( (RandomSourceValue)->T )
        
        //A bunch of boring convenience code here that transforms values so I 
don’t always have to switch on the enum in other code that deals with this. I 
just ask for the bounds or constrained T (Note: T here refers to the type for a 
single axis as opposed to the generated type. e.g. CGFloat for a point) 
    }

I have found that this handles pretty much all of the constraints I need, and 
the custom constraint is useful for anything exotic (e.g. sig-figs).  The 
RandomSource itself has convenience inits when T is Comparable that let you 
specify a range instead of having to create the constraints yourself.

I then have conformed many standard types to RandomSourceCreatable so that I 
can create Sources out of them.  Here is CGPoint for reference:

        extension CGPoint:RandomSourceCreatable {
    
        public static func dimension(given 
contraints:[String:RandomSourceConstraint<CGFloat>])->RandomSourceDimension {
                return RandomSourceDimension.manyWord(2)
        }
    
        public typealias ConstraintType = CGFloat
        public static var allowedConstraintKeys:Set<String>{
                return ["x","y"]
        }
    
        public static func createRandom(rnd value:RandomSourceValue, 
constraints:[String:RandomSourceConstraint<CGFloat>])->CGPoint {
                let xVal = value.value(at: 0)
                let yVal = value.value(at: 1)
                //Note: Ints have a better distribution for normal use cases of 
points
                let x = CGFloat(Int.createRandom(rnd: xVal, constraint: 
constraints["x"]?.asType({Int($0 * 1000)}) ?? .none))/1000
                let y = CGFloat(Int.createRandom(rnd: yVal, constraint: 
constraints["y"]?.asType({Int($0 * 1000)}) ?? .none))/1000
                return CGPoint(x: x, y: y)
        }
    }

Notice that I have a RandomSourceValue type that provides the random bits of 
the requested dimension. When I get around to updating this, I might do 
something closer to the proposal, where I would just pass the generator and 
grab bits as needed.  The main reason I did it the way I did is that it lets me 
have random access to the source very easily.  

The ‘asType’ method converts a constraint to work with another type (in this 
case Ints).

Colors are a bit more complicated, mainly because I allow a bunch of different 
constraints, and I also have validation code to make sure the constraints fit 
together properly. I also ask for different amounts of randomness based on 
whether it is greyscale or contains alpha. Just to give you a sense, here are 
the allowed constraint keys for a CGColor:
        
        public static var allowedConstraintKeys:Set<String>{
        return ["alpha","gray","red","green","blue", "hue", "saturation", 
"brightness"]
    }

and here is the creation method when the keys are for RGBA (I have similar 
sections for HSBA and greyscale):

        let rVal = value.value(at: 0)
    let gVal = value.value(at: 1)
    let bVal = value.value(at: 2)
    let aVal = value.value(at: 3)
    let r = CGFloat.createRandom(rnd: rVal, constraint: constraints["red"] ?? 
.range(0,1))
    let g = CGFloat.createRandom(rnd: gVal, constraint: constraints["green"] ?? 
.range(0,1))
    let b = CGFloat.createRandom(rnd: bVal, constraint: constraints["blue"] ?? 
.range(0,1))
    let a = CGFloat.createRandom(rnd: aVal, constraint: constraints["alpha"] ?? 
.constant(1.0))
            
    return self.init(colorSpace: CGColorSpaceCreateDeviceRGB(), components: 
[r,g,b,a])!


The end result is that initializing a source of CGColors looks like this 
(either parameter can be omitted if desired):

        let colorSource:Source<CGColor> = Source(seed: optionalSeed, 
constraints:["saturation": .constant(0.4), "brightness": .constant(0.6)])

Anyway, I hope this was useful/informative.  I know the code is a bit messy, 
but I still find it enormously useful in practice.  I plan to clean it up when 
I find time, simplifying the RandomSourceValue stuff and moving from String 
Keys to a Struct with static functions for the constraints.  The new 
constraints will probably end up looking like this:

        let colorSource:Source<CGColor> = Source(seed: optionalSeed, 
constraints:[.saturation(0.4), .brightness(0.4...0.6)])

Thanks,
Jon


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

Reply via email to