ทำความเข้าใจเกี่ยวกับเลนส์
Optics เป็นรูปแบบที่ยืมมาจาก Haskell ที่ให้คุณซูมเข้าไปในวัตถุได้ กล่าวอีกนัยหนึ่ง คุณสามารถตั้งค่าหรือรับคุณสมบัติของอ็อบเจ็กต์ในลักษณะที่ใช้งานได้ ตามการใช้งาน ฉันหมายความว่าคุณสามารถตั้งค่าคุณสมบัติโดยไม่ทำให้เกิดการกลายพันธุ์ ดังนั้นแทนที่จะแก้ไขวัตถุดั้งเดิม วัตถุใหม่จะถูกสร้างขึ้นด้วยคุณสมบัติที่อัปเดต เชื่อฉันเถอะว่ามันไม่ได้ซับซ้อนอย่างที่คิด ?
เราต้องการโค้ด Swift เพียงเล็กน้อยเพื่อทำความเข้าใจทุกอย่าง
struct Address {
let street: String
let city: String
}
struct Company {
let name: String
let address: Address
}
struct Person {
let name: String
let company: Company
}
อย่างที่คุณเห็น มันเป็นไปได้ที่จะสร้างลำดับชั้นโดยใช้โครงสร้างเหล่านี้ บุคคลสามารถมีบริษัทและบริษัทมีที่อยู่ เช่น
let oneInfiniteLoop = Address(street: "One Infinite Loop", city: "Cupertino")
let appleInc = Company(name: "Apple Inc.", address: oneInfiniteLoop)
let steveJobs = Person(name: "Steve Jobs", company: appleInc)
ตอนนี้ ลองจินตนาการว่าชื่อถนนของที่อยู่เปลี่ยนไป เราจะแก้ไขฟิลด์เดียวนี้และเผยแพร่การเปลี่ยนแปลงคุณสมบัติสำหรับโครงสร้างทั้งหมดได้อย่างไร ?
struct Address {
var street: String
let city: String
}
struct Company {
let name: String
var address: Address
}
struct Person {
let name: String
var company: Company
}
var oneInfiniteLoop = Address(street: "One Infinite Loop", city: "Cupertino")
var appleInc = Company(name: "Apple Inc.", address: oneInfiniteLoop)
var steveJobs = Person(name: "Steve Jobs", company: appleInc)
oneInfiniteLoop.street = "Apple Park Way"
appleInc.address = oneInfiniteLoop
steveJobs.company = appleInc
print(steveJobs)
ในการอัปเดตคุณสมบัติถนน เราต้องทำงานค่อนข้างมาก อันดับแรก เราต้องเปลี่ยนคุณสมบัติบางอย่างเป็นตัวแปร และเราต้องอัปเดตการอ้างอิงทั้งหมดด้วยตนเอง เนื่องจากโครงสร้างไม่ใช่ประเภทอ้างอิง แต่เป็นประเภทค่า ดังนั้นจึงมีการใช้สำเนาทั่วๆ ไป
สิ่งนี้ดูแย่มาก เราทำให้เกิดการกลายพันธุ์ค่อนข้างมาก และตอนนี้คุณสมบัติอื่นๆ สามารถเปลี่ยนคุณสมบัติของตัวแปรเหล่านี้ได้ ซึ่งเราไม่ต้องการ มีวิธีที่ดีกว่า? ดี…
let newSteveJobs = Person(name: steveJobs.name,
company: Company(name: appleInc.name,
address: Address(street: "Apple Park Way",
city: oneInfiniteLoop.city)))
โอเค นี่มันไร้สาระ เรามาทำอะไรดีๆ กันดีกว่าไหม? ?
เลนส์
เราสามารถใช้เลนส์เพื่อซูมคุณสมบัติและใช้เลนส์นั้นสร้างประเภทที่ซับซ้อนได้ เลนส์คือค่าที่แสดงแผนที่ระหว่างประเภทที่ซับซ้อนและคุณสมบัติอย่างใดอย่างหนึ่ง
ให้มันเรียบง่ายและกำหนดโครงสร้างเลนส์ที่สามารถแปลงวัตถุทั้งหมดเป็นค่าบางส่วนโดยใช้ getter และตั้งค่าบางส่วนบนวัตถุทั้งหมดโดยใช้ตัวตั้งค่า จากนั้นส่งคืน “วัตถุทั้งหมด” ใหม่ นี่คือลักษณะที่ความละเอียดของเลนส์ใน Swift
struct Lens<Whole, Part> {
let get: (Whole) -> Part
let set: (Part, Whole) -> Whole
}
ตอนนี้ เราสามารถสร้างเลนส์ที่ซูมคุณสมบัติถนนของที่อยู่ และสร้างที่อยู่ใหม่โดยใช้ที่อยู่ที่มีอยู่
let oneInfiniteLoop = Address(street: "One Infinite Loop", city: "Cupertino")
let appleInc = Company(name: "Apple Inc.", address: oneInfiniteLoop)
let steveJobs = Person(name: "Steve Jobs", company: appleInc)
let addressStreetLens = Lens<Address, String>(get: { $0.street },
set: { Address(street: $0, city: $1.city) })
let newSteveJobs = Person(name: steveJobs.name,
company: Company(name: appleInc.name,
address: addressStreetLens.set("Apple Park Way", oneInfiniteLoop)))
เรามาลองสร้างเลนส์สำหรับคุณสมบัติอื่นๆ กันด้วย
let oneInfiniteLoop = Address(street: "One Infinite Loop", city: "Cupertino")
let appleInc = Company(name: "Apple Inc.", address: oneInfiniteLoop)
let steveJobs = Person(name: "Steve Jobs", company: appleInc)
let addressStreetLens = Lens<Address, String>(get: { $0.street },
set: { Address(street: $0, city: $1.city) })
let companyAddressLens = Lens<Company, Address>(get: { $0.address },
set: { Company(name: $1.name, address: $0) })
let personCompanyLens = Lens<Person, Company>(get: { $0.company },
set: { Person(name: $1.name, company: $0) })
let newAddress = addressStreetLens.set("Apple Park Way", oneInfiniteLoop)
let newCompany = companyAddressLens.set(newAddress, appleInc)
let newPerson = personCompanyLens.set(newCompany, steveJobs)
print(newPerson)
สิ่งนี้อาจดูแปลกไปเล็กน้อยตั้งแต่แรกเห็น แต่เราแค่เกาพื้นผิวที่นี่ เป็นไปได้ที่จะจัดองค์ประกอบเลนส์และสร้างการเปลี่ยนจากวัตถุเป็นคุณสมบัติอื่นภายในลำดับชั้น
struct Lens<Whole, Part> {
let get: (Whole) -> Part
let set: (Part, Whole) -> Whole
}
extension Lens {
func transition<NewPart>(_ to: Lens<Part, NewPart>) -> Lens<Whole, NewPart> {
.init(get: { to.get(get($0)) },
set: { set(to.set($0, get($1)), $1) })
}
}
let personStreetLens = personCompanyLens.transition(companyAddressLens)
.transition(addressStreetLens)
let newPerson = personStreetLens.set("Apple Park Way", steveJobs)
print(newPerson)
ดังนั้น ในกรณีของเรา เราสามารถคิดวิธีการเปลี่ยนผ่านและสร้างเลนส์ระหว่างบุคคลกับทรัพย์สินของถนน ซึ่งจะทำให้เราสามารถปรับเปลี่ยนถนนได้โดยตรงโดยใช้เลนส์ที่สร้างขึ้นใหม่นี้
อ้อ อีกอย่าง เรายังขยายโครงสร้างเดิมเพื่อให้เลนส์เหล่านี้เป็นค่าเริ่มต้นได้ ?
extension Address {
struct Lenses {
static var street: Lens<Address, String> {
.init(get: { $0.street },
set: { Address(street: $0, city: $1.city) })
}
}
}
extension Company {
struct Lenses {
static var address: Lens<Company, Address> {
.init(get: { $0.address },
set: { Company(name: $1.name, address: $0) })
}
}
}
extension Person {
struct Lenses {
static var company: Lens<Person, Company> {
.init(get: { $0.company },
set: { Person(name: $1.name, company: $0) })
}
static var companyAddressStreet: Lens<Person, String> {
Person.Lenses.company
.transition(Company.Lenses.address)
.transition(Address.Lenses.street)
}
}
}
let oneInfiniteLoop = Address(street: "One Infinite Loop", city: "Cupertino")
let appleInc = Company(name: "Apple Inc.", address: oneInfiniteLoop)
let steveJobs = Person(name: "Steve Jobs", company: appleInc)
let newPerson = Person.Lenses.companyAddressStreet.set("Apple Park Way", steveJobs)
print(newPerson)
ในไซต์การโทร เราสามารถใช้บรรทัดเดียวในการอัปเดตคุณสมบัติถนนของโครงสร้างที่ไม่เปลี่ยนรูปได้ แน่นอนว่าเรากำลังสร้างสำเนาใหม่ของวัตถุทั้งหมด แต่นั่นก็ดีเพราะเราต้องการหลีกเลี่ยงการกลายพันธุ์ แน่นอน เราต้องสร้างเลนส์ค่อนข้างมากเพื่อให้มายากลนี้เกิดขึ้นภายใต้ประทุน แต่บางครั้งก็คุ้มค่ากับความพยายาม ☺️
ปริซึม
ตอนนี้เรารู้วิธีตั้งค่าคุณสมบัติของลำดับชั้นของโครงสร้างโดยใช้เลนส์แล้ว ให้ฉันแสดงประเภทข้อมูลอีกประเภทหนึ่งที่เราสามารถใช้แก้ไขค่า enum ได้ ปริซึมก็เหมือนกับเลนส์ แต่ใช้ได้กับประเภทผลรวม เรื่องสั้นโดยย่อ enums คือประเภทผลรวม โครงสร้างคือประเภทผลิตภัณฑ์ และความแตกต่างที่สำคัญคือจำนวนค่าที่ไม่ซ้ำกันที่คุณสามารถแสดงด้วยค่าเหล่านี้ได้
struct ProductExample {
let a: Bool
let b: Int8
}
enum SumExample {
case a(Bool)
case b(Int8)
}
ความแตกต่างอีกประการหนึ่งคือตัวรับปริซึมสามารถคืนค่าศูนย์และตัวตั้งค่าสามารถ “ล้มเหลว” ซึ่งหมายความว่าหากไม่สามารถตั้งค่าของคุณสมบัติได้ก็จะคืนค่าข้อมูลเดิมแทน
struct Prism<Whole, Part> {
let tryGet: (Whole) -> Part?
let inject: (Part) -> Whole
}
นี่คือวิธีที่เราใช้ปริซึม เราเรียกว่า getter tryGet
เนื่องจากจะส่งกลับค่าทางเลือก ตัวตั้งค่าจึงถูกเรียก inject
เพราะเราพยายามที่จะฉีดค่าบางส่วนใหม่และคืนค่าทั้งหมดถ้าเป็นไปได้ ผมขอแสดงตัวอย่างเพื่อให้เข้าใจมากขึ้น
enum State {
case loading
case ready(String)
}
extension State {
enum Prisms {
static var loading: Prism<State, Void> {
.init(tryGet: {
guard case .loading = $0 else {
return nil
}
return ()
},
inject: { .loading })
}
static var ready: Prism<State, String> {
.init(tryGet: {
guard case let .ready(message) = $0 else {
return nil
}
return message
},
inject: { .ready($0) })
}
}
}
เราได้สร้างความเรียบง่าย State
enum รวมทั้งเราได้ขยายและเพิ่มเนมสเปซ Prism ใหม่เป็น enum ที่มีคุณสมบัติสแตติกสองแบบ ปริซึมคงที่หนึ่งอันสำหรับทุกกรณีที่เรามีใน State enum ดั้งเดิม เราสามารถใช้ปริซึมเหล่านี้เพื่อตรวจสอบว่าสถานะที่กำหนดมีค่าที่ถูกต้องหรือสร้างสถานะใหม่โดยใช้วิธีการฉีด
let loadingState = State.loading
let readyState = State.ready("I'm ready.")
let newLoadingState = State.Prisms.loading.inject(())
let newReadyState = State.Prisms.ready.inject("Hurray!")
let nilMessage = State.Prisms.ready.tryGet(loadingState)
print(nilMessage)
let message = State.Prisms.ready.tryGet(readyState)
print(message)
ไวยากรณ์ดูเหมือนจะแปลกไปเล็กน้อยตั้งแต่แรกเห็น แต่เชื่อฉันเถอะว่า Prisms มีประโยชน์มาก คุณยังสามารถใช้การแปลงกับปริซึมได้ แต่นั่นเป็นหัวข้อขั้นสูงสำหรับวันอื่น
อย่างไรก็ตาม คราวนี้ฉันอยากจะหยุดที่นี่ เนื่องจากเลนส์เป็นหัวข้อที่ค่อนข้างใหญ่ และฉันไม่สามารถครอบคลุมทุกอย่างในบทความเดียวได้ หวังว่าบทความเล็กๆ นี้จะช่วยให้คุณเข้าใจเลนส์และปริซึมได้ดีขึ้นเล็กน้อยโดยใช้ภาษาโปรแกรม Swift ?