Last active
September 6, 2017 11:38
-
-
Save broomburgo/bdc956243be2c3806a3d38e4d207008b to your computer and use it in GitHub Desktop.
code for the article "Lenses and Prisms in Swift: a pragmatic approach"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// source: https://broomburgo.github.io/fun-ios/post/lenses-and-prisms-in-swift-a-pragmatic-approach/ | |
protocol LensType { | |
associatedtype WholeType | |
associatedtype PartType | |
var get: (WholeType) -> PartType { get } | |
var set: (PartType,WholeType) -> WholeType { get } | |
init(get: @escaping (WholeType) -> PartType, set: @escaping (PartType,WholeType) -> WholeType) | |
} | |
struct Lens<Whole,Part>: LensType { | |
typealias WholeType = Whole | |
typealias PartType = Part | |
let get: (Whole) -> Part /// get the "focused" part | |
let set: (Part,Whole) -> Whole /// set a new value for the "focused" part | |
init(get: @escaping (Whole) -> Part, set: @escaping (Part,Whole) -> Whole) { | |
self.get = get | |
self.set = set | |
} | |
} | |
extension LensType { | |
func over(_ transform: @escaping (PartType) -> PartType) -> (WholeType) -> WholeType { | |
return { whole in self.set(transform(self.get(whole)),whole) } | |
} | |
func join<Subpart, OtherLens>(_ other: OtherLens) -> Lens<WholeType,Subpart> where OtherLens: LensType, OtherLens.WholeType == PartType, OtherLens.PartType == Subpart { | |
return Lens<WholeType,Subpart>( | |
get: { other.get(self.get($0)) }, | |
set: { (subpart: Subpart, whole: WholeType) -> WholeType in | |
self.set(other.set(subpart,self.get(whole)),whole) | |
}) | |
} | |
func zip<OtherPart, OtherLens>(_ other: OtherLens) -> Lens<WholeType,(PartType,OtherPart)> where OtherLens: LensType, OtherLens.WholeType == WholeType, OtherLens.PartType == OtherPart { | |
return Lens<WholeType,(PartType,OtherPart)>( | |
get: { (self.get($0),other.get($0)) }, | |
set: { other.set($0.1,self.set($0.0,$1)) }) | |
} | |
} | |
struct Company { | |
var name: String | |
var board: BoardOfDirectors | |
struct lens { | |
static let name = Lens<Company,String>( | |
get: { $0.name }, | |
set: { (p,w) in | |
var m_w = w | |
m_w.name = p | |
return m_w | |
}) | |
static let board = Lens<Company,BoardOfDirectors>( | |
get: { $0.board }, | |
set: { (p,w) in | |
var m_w = w | |
m_w.board = p | |
return m_w | |
}) | |
} | |
} | |
struct BoardOfDirectors { | |
var ceo: Employee | |
var cto: Employee | |
var cfo: Employee | |
var coo: Employee | |
struct lens { | |
static let ceo = Lens<BoardOfDirectors,Employee>( | |
get: { $0.ceo }, | |
set: { BoardOfDirectors(ceo: $0, cto: $1.cto, cfo: $1.cfo, coo: $1.coo) }) | |
static let cto = Lens<BoardOfDirectors,Employee>( | |
get: { $0.cto }, | |
set: { BoardOfDirectors(ceo: $1.ceo, cto: $0, cfo: $1.cfo, coo: $1.coo) }) | |
static let cfo = Lens<BoardOfDirectors,Employee>( | |
get: { $0.cfo }, | |
set: { BoardOfDirectors(ceo: $1.ceo, cto: $1.cto, cfo: $0, coo: $1.coo) }) | |
static let coo = Lens<BoardOfDirectors,Employee>( | |
get: { $0.coo }, | |
set: { BoardOfDirectors(ceo: $1.ceo, cto: $1.cto, cfo: $1.cfo, coo: $0) }) | |
} | |
} | |
struct Employee { | |
var name: String | |
var salary: Salary | |
struct lens { | |
static let name = Lens<Employee,String>( | |
get: { $0.name }, | |
set: { (p,w) in | |
var m_w = w | |
m_w.name = p | |
return m_w | |
}) | |
static let salary = Lens<Employee,Salary>( | |
get: { $0.salary }, | |
set: { (p,w) in | |
var m_w = w | |
m_w.salary = p | |
return m_w | |
}) | |
} | |
} | |
struct Salary { | |
var amount: Double | |
var bonus: Double | |
struct lens { | |
static let amount = Lens<Salary,Double>( | |
get: { $0.amount }, | |
set: { Salary(amount: $0, bonus: $1.bonus) }) | |
static let bonus = Lens<Salary,Double>( | |
get: { $0.bonus }, | |
set: { Salary(amount: $1.amount, bonus: $0) }) | |
} | |
} | |
var currentCompany = Company( | |
name: "Something inc.", | |
board: BoardOfDirectors( | |
ceo: Employee( | |
name: "Jane Doe", | |
salary: Salary( | |
amount: 100, | |
bonus: 10)), | |
cto: Employee( | |
name: "John Doe", | |
salary: Salary( | |
amount: 80, | |
bonus: 8)), | |
cfo: Employee( | |
name: "Jane Doe Jr.", | |
salary: Salary( | |
amount: 80, | |
bonus: 4)), | |
coo: Employee( | |
name: "John Doe Jr.", | |
salary: Salary( | |
amount: 80, | |
bonus: 4)))) | |
struct Repository { | |
static var getCompany: Company { | |
print("Retrieving company: \(currentCompany)\n") | |
return currentCompany | |
} | |
static func setCompany(_ company: Company) { | |
print("Saving company: \(company)\n") | |
currentCompany = company | |
} | |
} | |
func example1() { | |
var com = Repository.getCompany | |
com.board.ceo.salary.bonus *= 0.5 | |
Repository.setCompany(com) | |
} | |
extension Double { | |
static func average(_ values: Double...) -> Double { | |
return values.reduce(0, +)/Double(values.count) | |
} | |
} | |
func example2() { | |
var com = Repository.getCompany | |
let ceoSalary = com.board.ceo.salary | |
let cfoSalary = com.board.cfo.salary | |
com.board.cto.salary.amount = Double.average(ceoSalary.amount,cfoSalary.amount) | |
Repository.setCompany(com) | |
} | |
func example3() { | |
var com = Repository.getCompany | |
let cfoAmount = com.board.cfo.salary.amount | |
var cfoBonus = com.board.cfo.salary.bonus | |
if cfoBonus/cfoAmount < 0.06 { | |
cfoBonus = cfoAmount*0.06 | |
} | |
com.board.cfo.salary.bonus = cfoBonus | |
Repository.setCompany(com) | |
} | |
let onCEO = Company.lens.board.join(BoardOfDirectors.lens.ceo) | |
let onCTO = Company.lens.board.join(BoardOfDirectors.lens.cto) | |
let onCFO = Company.lens.board.join(BoardOfDirectors.lens.cfo) | |
let onSalaryAmount = Employee.lens.salary.join(Salary.lens.amount) | |
let onSalaryBonus = Employee.lens.salary.join(Salary.lens.bonus) | |
func updateCompany(with action: (Company) -> Company) { | |
Repository.setCompany(action(Repository.getCompany)) | |
} | |
func example1_lenses() { | |
updateCompany(with: onCEO.join(onSalaryAmount).over { $0*0.5 }) | |
} | |
func example2_lenses() { | |
updateCompany { | |
onCTO.join(onSalaryAmount) | |
.set(Double.average( | |
onCEO.join(onSalaryAmount).get($0), | |
onCFO.join(onSalaryAmount).get($0)), | |
$0) | |
} | |
} | |
func example3_lenses() { | |
updateCompany(with: onCFO.join(onSalaryAmount) | |
.zip(onCFO.join(onSalaryBonus)) | |
.over { (amount,bonus) in | |
if bonus/amount < 0.06 { | |
return (amount,amount*0.06) | |
} else { | |
return (amount,bonus) | |
} | |
}) | |
} | |
typealias AnyDict = [String:Any] | |
/* this is wrong | |
func anyDictLens<Part>(at key: String, as: Part) -> Lens<AnyDict,Part> { | |
return Lens<AnyDict,Part>( | |
get: { (whole: AnyDict) -> Part in whole[key] as! Part }, | |
set: { (part: Part, whole: AnyDict) -> AnyDict in | |
var m_dict = whole | |
m_dict[key] = part | |
return m_dict | |
}) | |
} | |
*/ | |
func anyDictLens<Part>(at key: String, as: Part) -> Lens<AnyDict,Part?> { | |
return Lens<AnyDict,Part?>( | |
get: { (whole: AnyDict) -> Part? in whole[key] as? Part }, | |
set: { (part: Part?, whole: AnyDict) -> AnyDict in | |
var m_dict = whole | |
m_dict[key] = part | |
return m_dict | |
}) | |
} | |
let dict: AnyDict = ["user" : ["name" : "Mr. Creosote"]] | |
let lens1 = anyDictLens(at: "user", as: AnyDict.self) | |
let lens2 = anyDictLens(at: "name", as: String.self) | |
/* this won't compile | |
let nameLens = lens1.join(lens2) | |
*/ | |
protocol PrismType { | |
associatedtype WholeType | |
associatedtype PartType | |
var tryGet: (WholeType) -> PartType? { get } | |
var inject: (PartType) -> WholeType { get } | |
init(tryGet: @escaping (WholeType) -> PartType?, inject: @escaping (PartType) -> WholeType) | |
} | |
struct Prism<Whole,Part>: PrismType { | |
typealias WholeType = Whole | |
typealias PartType = Part | |
let tryGet: (Whole) -> Part? /// get the part, if possible | |
let inject: (Part) -> Whole /// changes the value to reflect the part that's injected in | |
init(tryGet: @escaping (Whole) -> Part?, inject: @escaping (Part) -> Whole) { | |
self.tryGet = tryGet | |
self.inject = inject | |
} | |
} | |
extension PrismType { | |
func tryOver(_ transform: @escaping (PartType) -> PartType) -> (WholeType) -> WholeType? { | |
return { whole in self.tryGet(whole).map { self.inject(transform($0)) } } | |
} | |
func join<OtherPart, OtherPrism>(_ other: OtherPrism) -> Prism<WholeType,OtherPart> where OtherPrism: PrismType, OtherPrism.WholeType == PartType, OtherPrism.PartType == OtherPart { | |
return Prism<WholeType,OtherPart>( | |
tryGet: { self.tryGet($0).flatMap(other.tryGet) }, | |
inject: { self.inject(other.inject($0)) }) | |
} | |
func zip<OtherWhole, OtherPrism>(_ other: OtherPrism) -> Prism<(WholeType,OtherWhole),PartType> where OtherPrism: PrismType, OtherPrism.PartType == PartType, OtherPrism.WholeType == OtherWhole { | |
return Prism<(WholeType,OtherWhole),PartType>( | |
tryGet: { (whole,otherWhole) in | |
self.tryGet(whole) ?? other.tryGet(otherWhole) | |
}, | |
inject: { part in | |
(self.inject(part),other.inject(part)) | |
}) | |
} | |
} | |
func anyDictPrism<Part>(at key: String, as: Part.Type) -> Prism<AnyDict,Part> { | |
return Prism<AnyDict,Part>( | |
tryGet: { $0[key] as? Part }, | |
inject: { [key:$0] }) | |
} | |
let prism1 = anyDictPrism(at: "user", as: AnyDict.self) | |
let prism2 = anyDictPrism(at: "name", as: String.self) | |
let namePrism = prism1.join(prism2) | |
let name = namePrism.tryGet(dict) /// it's Mr. Creosote! | |
let prism3 = anyDictPrism(at: "weight", as: String.self) | |
let weightPrism = prism1.join(prism3) | |
let weight = weightPrism.tryGet(dict) /// this is nil | |
let otherDict = prism1.join(prism3).inject("200 kg") /// this is ["user": ["weight": "200 kg"]] | |
func dictOverride(_ first: AnyDict, _ second: AnyDict) -> AnyDict { | |
var m_dict = first | |
for (key,value) in second { | |
if let current = m_dict[key] { | |
if let firstAnyDict = current as? AnyDict, let secondAnyDict = value as? AnyDict { | |
m_dict[key] = dictOverride(firstAnyDict, secondAnyDict) | |
} else { | |
m_dict[key] = value | |
} | |
} else { | |
m_dict[key] = value | |
} | |
} | |
return m_dict | |
} | |
let fullDict = dictOverride(dict, otherDict) /// this is ["user": ["name": "Mr. Creosote", "weight": "200 kg"]] | |
let jsonExample1 = [ | |
"info" : [ | |
"image" : [ | |
"icon_urls" : [ | |
"https://image.org/1.png", | |
"https://image.org/2.png", | |
"https://image.org/3.png"]]]] | |
let jsonExample2 = [ | |
"info" : [ | |
"image" : [ | |
"icon_urls" : [String]()]]] | |
let jsonExample3 = [ | |
"info" : [ | |
"image" : [ | |
"WRONG_KEY" : [ | |
"https://image.org/1.png", | |
"https://image.org/2.png", | |
"https://image.org/3.png"]]]] | |
let infoDictPrism = anyDictPrism(at: "info", as: AnyDict.self) | |
let imageDictPrism = anyDictPrism(at: "image", as: AnyDict.self) | |
let iconURLsPrism = anyDictPrism(at: "icon_urls", as: [String].self) | |
let firstURLPrism = Prism<[String],String>( | |
tryGet: { $0.first }, | |
inject: { [$0] }) | |
let imageURLPrism1 = infoDictPrism | |
.join(imageDictPrism) | |
.join(iconURLsPrism) | |
.join(firstURLPrism) | |
let firstURL = imageURLPrism1.tryGet(jsonExample1) /// this is "https://image.org/1.png" | |
let noFirstURL = imageURLPrism1.tryGet(jsonExample2) /// this is nil | |
let stillNoFirstURL = imageURLPrism1.tryGet(jsonExample3) /// this is nil | |
let jsonExample4 = [ | |
"info" : [ | |
"main_image_icon_url" : "https://image.org/3.png", | |
"image" : [ | |
"icon_urls" : [ | |
"https://image.org/1.png", | |
"https://image.org/2.png", | |
"https://image.org/3.png"]]]] | |
let imageURLPrism2 = infoDictPrism.join(anyDictPrism(at: "main_image_icon_url", as: String.self)) | |
let finalURL1 = imageURLPrism2.zip(imageURLPrism1).tryGet(jsonExample4,jsonExample4) /// this is "https://image.org/3.png" | |
let finalURL2 = imageURLPrism2.zip(imageURLPrism1).tryGet(jsonExample1,jsonExample1) /// this is "https://image.org/1.png" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment