My code is attempting to implement FP in an efficient / readable manner into some of my video games. I realize this may be a bit subjective, but I feel there is enough merit / objectivity to be of benefit to the community.
Why I'm asking for a review:
I'm not sure that my code is executing FP / procedural principles in an efficient manner, meaning if my attempts at FP / procedural are not good enough to be worth switch from mutable-state programming...
Or, if there are simpler ways of implementing what I'm trying to do functionally (I'm looking for a reasonable amount efficiency / low-verbosity), that would be cool.
Goal:
To make a single, mutable var 'cat list'
; this list contains immutable Cat
instances that can battle one another.
It's done in Swift 2.2. Here is a link to an online playground for 2.2 with my program loaded. Otherwise, you can run it in any main.swift console or Xcode playground if desired:
Documentation:
//: Playground - noun: a place where cats can play (with rockets).
/* BATTLE CATS!! Fed up with being "cutified," one faction of furry-
friends has decided to destroy all servers and the
Internet!!
Tech-loving felines across the globe have
taken up arms to protect their fame, fortune, and
web-based addiction. Choose your side, load up, and
BATTLE WITH CATS!!!
Goal:
- 1. To make an immutable OOP-style Cat object with FP principles.
- 2. To make a single, mutable var 'cat list' that contains all Cat objects
- 3. To easily (non verbosely) have Cats battle each other, in a functional way.
Conventions:
camelCase:
- Global Mutable: gGlobalName
- External Pram: pramName
- Labels: labelName
- Named Closures: closureName()
- Enum Case: caseName
PascalCase:
- Struct/Type: ClassName
under_score:
- Locals: local_name
- Parameter: pram_name
UPPERCASE:
- Constant Fields
(Non-transform): FIELDNAME
- Global Constant: gCONSTANT
Methods / Funcs:
- Are non-mutating, pass in a value, and always return a value.
However, named closures do not necessarily return a value
or pass in data (they are convenience).
- Pass 'self' "implicitly" (to reduce verbosity, etc.)
I still consider this "pure", though not referentailly transparent,
Since explicitly passing a `Cat` could produce the wrong expected outcome.
- Sub-functions may not always pass outer-pram for
verbose reasons ( see matchName() )
- Data used in the 'return' call or logic expressions
are usually declared and commented as "Returners" wih no value;
the subsequent `logicLabel do:' will assign the value.
Structs:
- 'Cat' instances transform via constructor with
default-parameters; a passed in "copyCat" pram
is used to copy from.
This is a psuedo-oop struct, but with FP principles.
The main initializer is a series of default prams set to nil,
and an 'copyCat' pram that is used to copy from. By
setting a pram to Not Nil, you effectively transform only one field.
CatList and Battle structs are not required to make Cats battle.
- 'CatList' is to be used as a singleton, but is handled
in a similar way to Cat--but currently CatList
has no methods (it is a data structure of Cat instances).
- 'Battle' is not necessary, but was created to make handling CatList and Cat
more easily and less verbosely. It is a "static func-struct" (no fields);
essentially a "utility class" from Java, etc.
TODO:
- Make more enums (alive, dead, etc)
- Implement random battles (unnamed cats)
- Test performance
- Make unit tests (not assertions)
- Simplify handleResults()
NOTE:
- Many, MANY, virtual cats were harmed in the making
of this program ;)
*/
Main struct:
/** Says meow... Then fires a rocket at your face: */
struct Cat {
// TODO: Add enooms for Faction, Status, etc.
/* Fields: */
// Transformable:
let age: Int,
name: String,
rockets: Int, // Our kitty is srsbznz... bin' warn3d..
lives: Int,
status: String,
hp: Int,
dmg_to_give: Int
// Constants:
let AP = 40,
DEF = 20,
MAXHP = 50
// Name Closures:
let meow = { print($0) }
/* Inits: */
/// Default init for new cat. Use 'init(fromOldCat:)' to transform.
init(newCatWithName _name: String) {
age = 5; name = _name; rockets = 5; lives = 9;
dmg_to_give = 0; hp = self.MAXHP; status = "Alive"
meow(" Nyaa! Watashi wa \(self.name) desu~!") // Purrrr...
}
/// Call for FP transformation:
init(copyCat cc: Cat,
age: Int? = nil,
name: String? = nil,
rockets: Int? = nil,
lives: Int? = nil,
status: String? = nil,
hp: Int? = nil,
dmg_to_give: Int? = nil) {
// Basics:
age == nil ? (self.age = cc.age) : (self.age = age! )
name == nil ? (self.name = cc.name) : (self.name = name! )
rockets == nil ? (self.rockets = cc.rockets) : (self.rockets = rockets!)
lives == nil ? (self.lives = cc.lives) : (self.lives = lives! )
status == nil ? (self.status = cc.status) : (self.status = status! )
hp == nil ? (self.hp = cc.hp) : (self.hp = hp! )
// Battle:
dmg_to_give == nil ? (self.dmg_to_give = cc.dmg_to_give):(self.dmg_to_give = dmg_to_give!)
// New cat purrs...
if (self.name != cc.name) { meow(" Nyaa! Watashi wa \(self.name) desu~!") }
}
/* Methods: */ // NOTE: All methods pass 'self' into parameter "implicitly" (pseudo-pure).
/** Calculates damage to give (from $0's DEF), then stores it in own field:
cat1 = cat1.fireRocket(at: cat2)
*/
func fireRocket(at victim: Cat) -> Cat {
// Returners:
let dmg_to_give2 = (self.AP - victim.DEF)
let rockets2 = (self.rockets - 1)
// TODO: Add a self.rockets check before firing a rocket
return Cat(copyCat: self, rockets: rockets2, dmg_to_give: dmg_to_give2)
}
/** Decreases own HP from value stored in other cat, then updates
cat2 = cat2.takeDamage(from: cat1)
*/
func takeDamage(from attacker: Cat) -> Cat {
// Returners:
let hp2: Int,
lives2: Int,
status2: String
assignmentLogic: do {
// Logic fodder:
let dmg_taken = attacker.dmg_to_give
let dam_left = (self.hp - dmg_taken)
// Our cat dies:
if dam_left <= 0 {
hp2 = self.MAXHP
lives2 = (self.lives - 1)
lives2 == 0 ? (status2 = "Dead") : (status2 = "Alive")
}
// Our cat lives:
else {
hp2 = dam_left
lives2 = self.lives
status2 = "Alive"
}
}
return Cat(copyCat: self, hp: hp2, lives: lives2, status: status2 )
}
/** Should be called after attacking cat uses a '.attack()', to reset dmgToGive.
cat1 = cat1.readyForNextBattle()
*/
func readyForNextBattle(/* Uses 'self'*/) -> Cat {
return Cat(copyCat: self, dmg_to_give: 0)
}
// End of Cat />
}
First test:
/* First Test */
// Makes transformation more obvious than '=' operator.. lhs 'does' rhs (transforms into):
infix operator ->>{}; func ->> <T> (inout l: T, r: T) { l = r }
/* This test is what prompted me to make CatList and Battle (defined below), due
to the verbose / inflexibility of this code:
*/
test1: do {
print("Test1: No Battle struct or CatList struct:")
var mittens = Cat(newCatWithName: "Mittens")
var nyan = Cat(newCatWithName: "Nyan")
mittens ->> mittens.fireRocket(at: nyan)
nyan ->> nyan.takeDamage(from: mittens)
mittens ->> mittens.readyForNextBattle()
// TODO: Refactor to nyan ->> mittens.FireRocket(at: nyan) to save 2 constructor calls
assert(nyan.hp == 30, "Failed")
print ("Nyan HP is 30 after battle: \(nyan.hp)\n")
}
Second part of code (not as important; attempts to make Test1
better):
/* List of Cats: */
/// Holds our cats:
struct CatList {
// TODO: Enum.case.rawValue seemed too verbose in the constructor, but Enum would be ideal..
struct Names { static let fluffy="Fluffy", kitty="Kitty", boots="Boots"}
// Known cats:
let fluffy: Cat,
kitty: Cat,
boots: Cat
// Unknown cats (random battles): // TODO: Implement random battles with unnamed cats
let uk_cats: [Cat]
// Default: // TODO: Make singleton:
private init(defaults: Int) {
fluffy = Cat( newCatWithName: Names.fluffy )
kitty = Cat( copyCat: fluffy, name: Names.kitty )
boots = Cat( copyCat: kitty , name: Names.boots )
uk_cats = [] // TODO: Something with this..
}
// Add named cats here:
init(copyCatList ccl: CatList, fluffy: Cat? = nil, kitty: Cat? = nil, boots: Cat? = nil, uk_cats: [Cat]? = nil) {
fluffy == nil ? (self.fluffy = ccl.fluffy) : (self.fluffy = fluffy! )
kitty == nil ? (self.kitty = ccl.kitty) : (self.kitty = kitty! )
boots == nil ? (self.boots = ccl.boots) : (self.boots = boots! )
uk_cats == nil ? (self.uk_cats = ccl.uk_cats) : (self.uk_cats = uk_cats!)
}
}
/* Global: */
/// Instance to pass around:
var gCatList: CatList
/* Battle funk */
/** Static / abstract struct (func only):
gCatList = Battle.start(handleResults(doCombat()))
*/
struct Battle {
/// 1v1 Combatants:
typealias Combatants = (attacker: Cat, victim: Cat)
/// Makes doCombat (defined next) more Englishy / fun, by pairing an enum to a 1v1 Cat.method()
enum Attacks {
case fires1Rocket
func becomeFunc(combatants: Combatants) -> ()->(Cat) {
let attacker = combatants.attacker
let victim = combatants.victim
switch self {
case fires1Rocket:
return { attacker.fireRocket( at: victim) }
}
}
}
/** Returns two new cats after battling.. Use them to update your CatList:
results = doCombat()
*/
private static func doCombat(attacker: Cat, _ atk_const: Attacks, at victim: Cat)
-> Combatants {
// Because we need to need execute the attack:
let attacker2: ()->(Cat) = atk_const.becomeFunc((attacker, victim))
// Returners (Cats):
let that_attacked: Cat = attacker2().readyForNextBattle()
let that_was_victimized: Cat = victim.takeDamage(from: attacker2())
return (attacker: that_attacked, victim: that_was_victimized)
}
/** Mutates our gCatList automagically with the battle results:
updated_cat_list = handleResults()
*/
private static func handleResults(battled_cats battled_cats: Combatants,
fromInitial list: CatList)
-> CatList {
// Returner method:
func matchName(this_cat: (name: String, is_attacker: Bool),
updateFrom list2: CatList
/* battled_cats: Combatants */)
-> CatList {
// Returner:
let new_cat: Cat
// Logic1:
this_cat.is_attacker ?
(new_cat = battled_cats.attacker) : (new_cat = battled_cats.victim)
// Logic2:
typealias n=CatList.Names
switch this_cat.name {
case n.boots: return CatList (copyCatList: list2, boots: new_cat)
case n.fluffy: return CatList (copyCatList: list2, fluffy: new_cat)
case n.kitty: return CatList (copyCatList: list2, kitty: new_cat)
default: fatalError("cant find cat's name. Should use an Enum")
}
}
// Returners:
let attacker2 = (name: battled_cats.attacker.name, is_attacker: true)
let victim2 = (name: battled_cats.victim.name , is_attacker: false)
// Attacker must present 'dmg_to_give' to victim;
// therefore, attacker must be processed first (on the right):
return matchName( victim2, updateFrom: (matchName(attacker2, updateFrom: list)) )
}
/** Brings all private funcs together (in reverse order):
#### usage:
gCatList = (start -> handleResults -> doCombat)
- NOTE:
Reads from global in default pram for convenience (no mutate):
*/
static func start(combatants: Combatants,
initialList gcat_list: CatList = gCatList)
-> CatList {
let attacker = combatants.attacker
let victim = combatants.victim
return handleResults(battled_cats: doCombat( attacker, .fires1Rocket, at: victim ),
fromInitial: gcat_list)
}
}
Final testing:
/* TESTING: 2-4 */
// Tests 2 - 4. Fluffy is the Victim
aBattleTests: do {
let victim_hp = "Victim's HP after battle (should be 30): "
let assertHP30 = { assert(gCatList.fluffy.hp == 30, "Failed") }
test2: do {
// Most verbose way (but has most clarity / readability):
print("Test2: Verbose Battle Struct:")
gCatList = CatList(defaults: 1)
let test_attacker = gCatList.boots
let test_victim = gCatList.fluffy
let between_combatants = (test_attacker, test_victim)
gCatList->>Battle.start(between_combatants)
assertHP30()
print(victim_hp, gCatList.fluffy.hp, "\n")
}
test3: do {
// Verbose way:
print("Test3: ")
gCatList = CatList(defaults: 1)
gCatList->>Battle.start((attacker: gCatList.boots, victim: gCatList.fluffy))
assertHP30()
print(victim_hp, gCatList.fluffy.hp, "\n")
}
test4: do {
// Least verbose way: (force error)
print("Test4: Assertion Failure:")
gCatList = CatList(defaults: 0)
gCatList->>Battle.start((gCatList.boots, gCatList.boots))
assertHP30()
print(victim_hp, gCatList.fluffy.hp, "\n")
}
}
I found that a better way might be to just do something like:
nyan = fluffy.attack(victim: nyan)
The above would get rid of 2 constructor calls... But the main point of this program was to test the general idea of default prams in constructors, not necessarily the most efficient way to battle with cats.
Also, using nil
as a default pram in the initializer is SUPER slow. It's a lot faster (but more verbose) to use a regular init
that's called through a makeNewCat(copyCat: Cat) -> Cat
function (about 4 times faster)...
It's starting to look like to me that any code that has heavy mutation simply is going to be much faster with final class X { var xField: Type; func mutateX() }
, as it scales at almost f(x) = 1 compared to 1:1 scaling with initializer transformations (more fields = slower performance).
TLDR:
Right now, I'm leaning towards mutability for the heavy-hitters in GamePlay
code (players, enemies, etc), and immutability for things like assignment logic / algorithms.
I haven't found a fast enough way (within say 20% of final class
) to transform a data set (20-50 fields), and the scaling issues are absolutely tragic. Dictionaries are supposed to be super fast, so perhaps struct
isn't the way to go (outside of a namespace for everything for caching reasons).
Granted, things in SpriteKit / SceneKit etc are already NSObjects
and are slow as crud due to OBJc. I will probably never actually need any of this performance I'm crying about. Just where I'm at right now.
mutating
for structs. The structure is still immutable, but then Swift handles the copy/assign for you. That, and it's time to update to Swift 3 😊 If I have time I'll come give this a more detailed review: it looks interesting. \$\endgroup\$var
and having a mutating method? Thanks for reading :) \$\endgroup\$