Solving problems without loops.....
This post is part of a series:
Functional Adventures in F# - A simple planner
Functional Adventures in F# - Calling F# from C#
Functional Adventures in F# - Using Map Collections
Functional Adventures in F# - Application State
Functional Adventures in F# - Types with member functions
Functional Adventures in F# - Getting rid of loops
Functional Adventures in F# - Getting rid of temp variables
Functional Adventures in F# - The MailboxProcessor
Functional Adventures in F# - Persisting Application State
Functional Adventures in F# - Adding Snapshot Persisting
Functional Adventures in F# - Type-safe identifiers
Functional Adventures in F# - Event sourcing lessons learned
This time lets look at a real issue when coming from an imperative language and trying functional programming.
The example problem (that I crashed into when rewriting some game logic in F#) is that of spawning new Ships in a space game. We do not want to spawn 2 ships on top of each other, so we generate the default spawning coordinate and then test if it is safe to use, if not we move the ship a bit and try again....
How I solved it in C#..
private Ship FindClosestFreeCoordinate(ImmutableList<Fleet> state, Ship newShip) { var shipsWithinSafetyDistance = ShipsWithinSafetyDistance(state, newShip); while (shipsWithinSafetyDistance.Any()) { var newCoordinates = newShip.Coordinates.Position.Add(Vector3.UnitXVector); newShip = newShip.Modify(coordinates: newShip.Coordinates.Modify(position: newCoordinates)); shipsWithinSafetyDistance = ShipsWithinSafetyDistance(state, newShip); } return newShip; } private List<Ship> ShipsWithinSafetyDistance(ImmutableList<Fleet> state, Ship newShip) { return (from fleet in state from ship in fleet.Ships where ship.Distance(newShip) < 5 select ship).ToList(); }
Translating the ShipsWithinSafetyDistance function is no problem. We just use the Map.filter function...
static member private shipsWithinSafetyDistance (unit:Unit) (state:UnitStore) = Map.filter (fun key value -> unit.distanceToUnit value < 5.0) state.UnitsAh, yes.. I switched from a ImmutableList in C# to a Map in F#, just to keep things interesting (and to get faster Key lookups)
So, the tricky part here is the while loop. Most loops can be rewritten with Map/List functions like map, filter and fold etc... But this time I don't see how to use them as I don't have a Collection of any sort here so my brain keeps yelling "Write a loooooop", as that is the way I have solved this kind of problems for the past 20+ years... So...
How should we do it in F#??? We have some other constructs to use that fit the functional idea better.
static member private findClosestFreeCoordinate (unit:Unit) (state:UnitStore) = let rec findClosestFreeCoordinateRecursive (unit:Unit) (state:UnitStore) = let shipsWithinSafetyDistance = shipsWithinSafetyDistance unit state match shipsWithinSafetyDistance with | a when a.IsEmpty -> unit | _ -> let newUnit = { unit with Coordinates = unit.Coordinates + Vector3.UnitX } findClosestFreeCoordinateRecursive newUnit state findClosestFreeCoordinateRecursive unit stateRecursion!
At least for this problem, it seems to be a good option. Here we define a function, findClosestFreeCoordinateRecursive with the rec keyword to let the compiler know that it will be called recursively. In the function body we match the result list with IsEmpty and return the input value or else call the recursive function with a new copy of the unit with new coordinates.
Call stack.. Is there a risk for stack overflow with this?
Lets open the dll output with ILSpy to check what actually goes on behind the scenes
So there, lets move the last shipsWithinSafetyDistance to inner scope of our function and we are set
static member private findClosestFreeCoordinate (unit:Unit) (state:UnitStore) = let shipsWithinSafetyDistance (unit:Unit) (state:UnitStore) = Map.filter (fun _ value -> unit.distanceToUnit value < 50.0) state.Units let rec findClosestFreeCoordinateRecursive (unit:Unit) (state:UnitStore) = let shipsWithinSafetyDistance = shipsWithinSafetyDistance unit state match shipsWithinSafetyDistance with | a when a.IsEmpty -> unit | _ -> let newUnit = { unit with Coordinates = unit.Coordinates + Vector3.UnitX } findClosestFreeCoordinateRecursive newUnit state findClosestFreeCoordinateRecursive unit state
All code provided as-is. This is copied from my own code-base, May need some additional programming to work. Use for whatever you want, how you want! If you find this helpful, please leave a comment, not required but appreciated! :)
Hope this helps someone out there!
Good rreading
ReplyDelete