เลนส์และปริซึมในภาษา Swift

 

ทำความเข้าใจเกี่ยวกับเลนส์

 

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)))

 

See also  การเรียนรู้ SwiftUI สำหรับ iOS 16 และ Xcode 14 ได้รับการเผยแพร่แล้ว

เรามาลองสร้างเลนส์สำหรับคุณสมบัติอื่นๆ กันด้วย

 

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)

 

See also  ios - แสดง UIView เป็น CMSampleBuffer ใน Swift

ในไซต์การโทร เราสามารถใช้บรรทัดเดียวในการอัปเดตคุณสมบัติถนนของโครงสร้างที่ไม่เปลี่ยนรูปได้ แน่นอนว่าเรากำลังสร้างสำเนาใหม่ของวัตถุทั้งหมด แต่นั่นก็ดีเพราะเราต้องการหลีกเลี่ยงการกลายพันธุ์ แน่นอน เราต้องสร้างเลนส์ค่อนข้างมากเพื่อให้มายากลนี้เกิดขึ้นภายใต้ประทุน แต่บางครั้งก็คุ้มค่ากับความพยายาม ☺️

 

 


ปริซึม

 

ตอนนี้เรารู้วิธีตั้งค่าคุณสมบัติของลำดับชั้นของโครงสร้างโดยใช้เลนส์แล้ว ให้ฉันแสดงประเภทข้อมูลอีกประเภทหนึ่งที่เราสามารถใช้แก้ไขค่า 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 ?

 

 

By C-MTI