kelan.io

Regions - Functional Approach

import Darwin
struct Position { let x, y: Float }
typealias Distance = Float


// This is the key insight, modelling the region as a function.  (But, I think it's also hard to think this way when you're used to OO).
typealias Region = Position -> Bool


// Define a circle (centered at origin)
func circle(radius: Distance) -> Region
{
    return { point in
        hypot(point.x, point.y) < radius
    }
}

// might as well make a rect too
func rect(width: Distance, height: Distance) -> Region
{
    return { point in
        fabs(point.x) <= width/2 && fabs(point.y) <= height/2
    }
}


let unitCircle = circle(1.0)
unitCircle(Position(x: 0.5, y: 0.5))
unitCircle(Position(x: 2, y: 3))



// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// now, make a func that can transform a region

func shift(region: Region, by offset: Position) -> Region
{
    return { point in
        region(Position(x: point.x - offset.x, y: point.y - offset.y))
    }
}

let shiftedCircle = shift(unitCircle, by: Position(x: 1.5, y: 3.5))
shiftedCircle(Position(x: 0.5, y: 0.5))
shiftedCircle(Position(x: 2, y: 3))


// We can define other tranformations too

func invert(region: Region) -> Region
{
    return { point in !region(point) }
}

func intersection(of a: Region, with b: Region) -> Region
{
    return { point in a(point) && b(point) }
}

func union(of a: Region, with b: Region) -> Region
{
    return { point in a(point) || b(point) }
}

func difference(of region: Region, minusRegion: Region) -> Region
{
    return intersection(of: region, with: invert(minusRegion))
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Now, just go straight to the most complex example from before

let ownPosition = Position(x: 10, y: 12)
let weaponRange = circle(5.0)
let safeDistance = circle(1.0)
let friendlyPosition = Position(x: 12, y: 9)
let friendlyRegion = shift(safeDistance, by: friendlyPosition)

let shouldFireAtTarget = difference(
    of: shift(
        difference(
            of: weaponRange,
            minusRegion: safeDistance),
        by: ownPosition),
    minusRegion: friendlyRegion)


// Test it
shouldFireAtTarget(Position(x: 0, y: 0))  // too far away
shouldFireAtTarget(Position(x: 9, y: 15))  // hit!
shouldFireAtTarget(Position(x: 10.5, y: 12))  // too close to self
shouldFireAtTarget(Position(x: 12.25, y: 9.25))  // too close to friendly


// But, that fully-nested function composition is hard to parse (because you
// have to read it "inside-out").
// So, do the composition in separate steps, so it's easier to read.
let safeRange = difference(of: weaponRange, minusRegion: safeDistance)
let shiftedSafeRange = shift(safeRange, by: ownPosition)
let shouldFireAtTarget2 = difference(of: shiftedSafeRange, minusRegion: friendlyRegion)

// Test this version
shouldFireAtTarget2(Position(x: 0, y: 0))  // too far away
shouldFireAtTarget2(Position(x: 9, y: 15))  // hit!
shouldFireAtTarget2(Position(x: 10.5, y: 12))  // too close to self
shouldFireAtTarget2(Position(x: 12.25, y: 9.25))  // too close to friendly