প্রোটোকল-ভিত্তিক প্রোগ্রামিং & জেনেরিক

TensorFlow.org এ দেখুন Google Colab-এ চালান GitHub-এ উৎস দেখুন

এই টিউটোরিয়ালটি প্রোটোকল-ভিত্তিক প্রোগ্রামিং এবং প্রতিদিনের উদাহরণে জেনেরিকের সাথে কীভাবে ব্যবহার করা যেতে পারে তার বিভিন্ন উদাহরণের উপর যাবে।

প্রোটোকল

উত্তরাধিকার প্রোগ্রামিং ভাষায় কোড সংগঠিত করার একটি শক্তিশালী উপায় যা আপনাকে প্রোগ্রামের একাধিক উপাদানের মধ্যে কোড ভাগ করতে দেয়।

সুইফটে, উত্তরাধিকার প্রকাশ করার বিভিন্ন উপায় রয়েছে। আপনি ইতিমধ্যেই অন্যান্য ভাষা থেকে এই উপায়গুলির একটির সাথে পরিচিত হতে পারেন: শ্রেণি উত্তরাধিকার। যাইহোক, সুইফটের আরেকটি উপায় আছে: প্রোটোকল।

এই টিউটোরিয়ালে, আমরা প্রোটোকলগুলি অন্বেষণ করব - সাবক্লাসিংয়ের একটি বিকল্প যা আপনাকে বিভিন্ন ট্রেডঅফের মাধ্যমে একই লক্ষ্য অর্জন করতে দেয়। সুইফটে, প্রোটোকলগুলিতে একাধিক বিমূর্ত সদস্য থাকে। ক্লাস, স্ট্রাকট এবং এনামগুলি একাধিক প্রোটোকলের সাথে সামঞ্জস্য করতে পারে এবং কনফর্মেন্স সম্পর্কটি পূর্ববর্তীভাবে প্রতিষ্ঠিত হতে পারে। যা কিছু ডিজাইনকে সক্ষম করে যা সাবক্লাসিং ব্যবহার করে সুইফটে সহজে প্রকাশযোগ্য নয়। প্রোটোকলের (এক্সটেনশন এবং প্রোটোকল সীমাবদ্ধতা) এবং সেইসাথে প্রোটোকলের সীমাবদ্ধতাগুলিকে সমর্থন করে এমন বাগধারাগুলির মধ্য দিয়ে আমরা হাঁটব৷

সুইফট 💖 এর মান প্রকার!

রেফারেন্স শব্দার্থবিদ্যা আছে এমন ক্লাস ছাড়াও, সুইফ্ট মান দ্বারা পাস করা enums এবং structs সমর্থন করে। Enums এবং structs ক্লাস দ্বারা প্রদত্ত অনেক বৈশিষ্ট্য সমর্থন করে। একবার দেখা যাক!

প্রথমত, আসুন দেখি কিভাবে enums ক্লাসের অনুরূপ:

enum Color: String {
    case red = "red"
    case green = "green"
    case blue = "blue"
    // A computed property. Note that enums cannot contain stored properties.
    var hint: String {
        switch self {
            case .red:
                return "Roses are this color."
            case .green:
                return "Grass is this color."
            case .blue:
                return "The ocean is this color."
        }
    }

    // An initializer like for classes.
    init?(color: String) {
        switch color {
        case "red":
            self = .red
        case "green":
            self = .green
        case "blue":
            self = .blue
        default:
            return nil
        }
    }
}

// Can extend the enum as well!
extension Color {
    // A function.
    func hintFunc() -> String {
        return self.hint
    }
}

let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")

let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color.
is invalidColor nil: true

এখন, আসুন structs তাকান. লক্ষ্য করুন যে আমরা স্ট্রাকট উত্তরাধিকারী হতে পারি না, তবে পরিবর্তে প্রোটোকল ব্যবহার করতে পারি:

struct FastCar {
    // Can have variables and constants as stored properties.
    var color: Color
    let horsePower: Int
    // Can have computed properties.
    var watts: Float {
       return Float(horsePower) * 745.7
    }
    // Can have lazy variables like in classes!
    lazy var titleCaseColorString: String = {
        let colorString = color.rawValue
        return colorString.prefix(1).uppercased() + 
               colorString.lowercased().dropFirst()
    }()
    // A function.
    func description() -> String {
        return "This is a \(color) car with \(horsePower) horse power!"
    }
    // Can create a variety of initializers.
    init(color: Color, horsePower: Int) {
        self.color = color
        self.horsePower = horsePower
    }
    // Can define extra initializers other than the default one.
    init?(color: String, horsePower: Int) {
        guard let enumColor = Color(color: color) else {
            return nil
        }
        self.color = enumColor
        self.horsePower = horsePower
    }
}

var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power!
Horse power in watts: 186425.0
Red

অবশেষে, আসুন দেখি কিভাবে তারা ক্লাসের বিপরীতে মান প্রকারের দ্বারা পাস করে:

// Notice we have no problem modifying a constant class with 
// variable properties.
class A {
  var a = "a"
}

func foo(_ a: A) {
  a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)

/* 
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/

// func modify(car: FastCar, toColor color: Color) -> Void {
//   car.color = color
// }

// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())
a
foo

এর প্রোটোকল ব্যবহার করা যাক

আসুন বিভিন্ন গাড়ির জন্য প্রোটোকল তৈরি করে শুরু করি:

protocol Car {
    var color: Color { get set }
    var price: Int { get }
    func turnOn()
    mutating func drive()
}

protocol Electric {
    mutating func recharge()
    // percentage of the battery level, 0-100%.
    var batteryLevel: Int { get set }
}

protocol Gas {
    mutating func refill()
    // # of liters the car is holding, varies b/w models.
    var gasLevelLiters: Int { get set }
}

একটি অবজেক্ট-ওরিয়েন্টেড বিশ্বে (কোনও একাধিক উত্তরাধিকার ছাড়াই), আপনি হয়তো Electric এবং Gas বিমূর্ত ক্লাস তৈরি করেছেন তারপর উভয়কে Car থেকে উত্তরাধিকারী করতে ক্লাস ইনহেরিট্যান্স ব্যবহার করেছেন এবং তারপরে একটি নির্দিষ্ট গাড়ির মডেলকে একটি বেস ক্লাস হতে হবে। যাইহোক, এখানে উভয়ই জিরো কাপলিং সহ সম্পূর্ণ আলাদা প্রোটোকল! আপনি কীভাবে এটি ডিজাইন করেন তাতে এটি সম্পূর্ণ সিস্টেমটিকে আরও নমনীয় করে তোলে।

আসুন একটি টেসলা সংজ্ঞায়িত করা যাক:

struct TeslaModelS: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

var tesla = TeslaModelS(color: .red, price: 110000, batteryLevel: 100)

এটি একটি নতুন struct TeslaModelS নির্দিষ্ট করে যা Car এবং Electric উভয় প্রোটোকলের সাথে সামঞ্জস্যপূর্ণ।

এখন গ্যাস চালিত গাড়ির সংজ্ঞা দেওয়া যাক:

struct Mustang: Car, Gas{
    var color: Color
    let price: Int
    var gasLevelLiters: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Time to drive!")
        gasLevelLiters -= 1
    }

    mutating func refill() {
        print("Filling the tank...")
        gasLevelLiters = 25
    }
}

var mustang = Mustang(color: .red, price: 30000, gasLevelLiters: 25)

ডিফল্ট আচরণ সহ প্রোটোকল প্রসারিত করুন

উদাহরণগুলি থেকে আপনি যা লক্ষ্য করতে পারেন তা হল আমাদের কিছু অপ্রয়োজনীয়তা আছে। যখনই আমরা একটি বৈদ্যুতিক গাড়ি রিচার্জ করি, তখন আমাদের ব্যাটারি শতাংশের মাত্রা 100 এ সেট করতে হবে। যেহেতু সমস্ত বৈদ্যুতিক গাড়ির সর্বোচ্চ ক্ষমতা 100%, কিন্তু গ্যাসের গাড়ির গ্যাস ট্যাঙ্কের ক্ষমতার মধ্যে তারতম্য হয়, তাই আমরা বৈদ্যুতিক গাড়ির জন্য 100 স্তরে ডিফল্ট করতে পারি। .

এখানেই সুইফটের এক্সটেনশনগুলি কাজে আসতে পারে:

extension Electric {
    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

তাই এখন, আমরা যে কোনো নতুন বৈদ্যুতিক গাড়ি তৈরি করি যখন আমরা এটিকে রিচার্জ করি তখন ব্যাটারিটি 100 এ সেট করবে। এইভাবে, আমরা কেবলমাত্র অনন্য এবং ডিফল্ট আচরণের সাথে ক্লাস, স্ট্রাকস এবং এনামগুলি সাজাতে সক্ষম হয়েছি।

প্রটোকল কমিক

কমিকের জন্য রে ওয়েন্ডারলিচকে ধন্যবাদ!

যাইহোক, একটি বিষয় খেয়াল রাখতে হবে নিম্নোক্ত বিষয়গুলো। আমাদের প্রথম বাস্তবায়নে, আমরা foo() A তে একটি ডিফল্ট বাস্তবায়ন হিসাবে সংজ্ঞায়িত করি, কিন্তু প্রোটোকলে এটি প্রয়োজনীয় করে না। তাই যখন আমরা a.foo() কল করি, তখন আমরা " A default " প্রিন্ট পাই।

protocol Default {}

extension Default {
    func foo() { print("A default")}
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
A default

যাইহোক, যদি আমরা A তে foo() প্রয়োজনীয় করি, তাহলে আমরা " Inst " পাব:

protocol Default {
    func foo()
}

extension Default {
    func foo() { 
        print("A default")
    }
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
Inst

প্রথম উদাহরণে স্ট্যাটিক ডিসপ্যাচ এবং সুইফটের প্রোটোকলের দ্বিতীয়টিতে স্ট্যাটিক ডিসপ্যাচের মধ্যে পার্থক্যের কারণে এটি ঘটে। আরও তথ্যের জন্য, এই মিডিয়াম পোস্টটি পড়ুন।

ডিফল্ট আচরণ ওভাররাইডিং

যাইহোক, যদি আমরা চাই, আমরা এখনও ডিফল্ট আচরণ ওভাররাইড করতে পারি। উল্লেখ্য একটি গুরুত্বপূর্ণ বিষয় হল যে এটি গতিশীল প্রেরণ সমর্থন করে না

ধরা যাক আমাদের কাছে একটি বৈদ্যুতিক গাড়ির একটি পুরানো সংস্করণ রয়েছে, তাই ব্যাটারি স্বাস্থ্য 90% এ হ্রাস করা হয়েছে:

struct OldElectric: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func reCharge() {
        print("Recharging the battery...")
        batteryLevel = 90
    }
}

প্রোটোকলের স্ট্যান্ডার্ড লাইব্রেরি ব্যবহার

এখন যেহেতু সুইফটের প্রোটোকলগুলি কীভাবে কাজ করে সে সম্পর্কে আমাদের ধারণা আছে, আসুন স্ট্যান্ডার্ড লাইব্রেরি প্রোটোকল ব্যবহার করার কিছু সাধারণ উদাহরণ দিয়ে যাই।

স্ট্যান্ডার্ড লাইব্রেরি প্রসারিত করুন

চলুন দেখি কিভাবে আমরা সুইফটে আগে থেকেই বিদ্যমান টাইপের অতিরিক্ত কার্যকারিতা যোগ করতে পারি। যেহেতু সুইফটের প্রকারগুলি অন্তর্নির্মিত নয়, তবে স্ট্রাকট হিসাবে স্ট্যান্ডার্ড লাইব্রেরির অংশ, তাই এটি করা সহজ।

আসুন উপাদানগুলির একটি অ্যারেতে বাইনারি অনুসন্ধান করার চেষ্টা করি, পাশাপাশি অ্যারেটি সাজানো হয়েছে কিনা তা নিশ্চিত করুন:

extension Collection where Element: Comparable {
    // Verify that a `Collection` is sorted.
    func isSorted(_ order: (Element, Element) -> Bool) -> Bool {
        var i = index(startIndex, offsetBy: 1)

        while i < endIndex {
            // The longer way of calling a binary function like `<(_:_:)`, 
            // `<=(_:_:)`, `==(_:_:)`, etc.
            guard order(self[index(i, offsetBy: -1)], self[i]) else {
                return false
            }
            i = index(after: i)
        }
        return true
    }

    // Perform binary search on a `Collection`, verifying it is sorted.
    func binarySearch(_ element: Element) -> Index? {
        guard self.isSorted(<=) else {
            return nil
        }

        var low = startIndex
        var high = endIndex

        while low <= high {
            let mid = index(low, offsetBy: distance(from: low, to: high)/2)

            if self[mid] == element {
                return mid
            } else if self[mid] < element {
                low = index(after: mid)
            } else {
                high = index(mid, offsetBy: -1)
            }
        }

        return nil
    }
}

print([2, 2, 5, 7, 11, 13, 17].binarySearch(5)!)
print(["a", "b", "c", "d"].binarySearch("b")!)
print([1.1, 2.2, 3.3, 4.4, 5.5].binarySearch(3.3)!)
2
1
2

আমরা Collection প্রোটোকল প্রসারিত করে এটি করি যা সংজ্ঞায়িত করে "একটি ক্রম যার উপাদানগুলি একাধিকবার, অ-ধ্বংসাত্মকভাবে অতিক্রম করা যেতে পারে এবং একটি সূচীকৃত সাবস্ক্রিপ্ট দ্বারা অ্যাক্সেস করা যেতে পারে।" যেহেতু অ্যারেগুলি বর্গাকার বন্ধনী স্বরলিপি ব্যবহার করে সূচিত করা যেতে পারে, তাই এই প্রোটোকলটি আমরা প্রসারিত করতে চাই।

একইভাবে, আমরা শুধুমাত্র এই ইউটিলিটি ফাংশনটি অ্যারেগুলিতে যোগ করতে চাই যার উপাদানগুলি তুলনা করা যেতে পারে। এই কারণেই আমরা where Element: Comparable

where ক্লজটি সুইফটের টাইপ সিস্টেমের একটি অংশ, যা আমরা শীঘ্রই কভার করব, কিন্তু সংক্ষেপে আমরা যে এক্সটেনশনটি লিখছি তাতে অতিরিক্ত প্রয়োজনীয়তা যোগ করতে দেয়, যেমন একটি প্রোটোকল বাস্তবায়নের জন্য টাইপের প্রয়োজন, দুই ধরনের হতে হবে একই, বা একটি নির্দিষ্ট সুপারক্লাস থাকার জন্য একটি শ্রেণীর প্রয়োজন।

Element হল একটি Collection কনফর্মিং টাইপের উপাদানগুলির সংশ্লিষ্ট প্রকার। Element Sequence প্রোটোকলের মধ্যে সংজ্ঞায়িত করা হয়, কিন্তু যেহেতু Collection Sequence থেকে উত্তরাধিকারসূত্রে প্রাপ্ত হয়, তাই এটি Element সম্পর্কিত প্রকারের উত্তরাধিকারী হয়।

Comparable হল একটি প্রোটোকল যা সংজ্ঞায়িত করে "একটি প্রকার যা রিলেশনাল অপারেটর < , <= , >= , এবং > ব্যবহার করে তুলনা করা যেতে পারে।" . যেহেতু আমরা একটি সাজানো Collection বাইনারি অনুসন্ধান করছি, এটি অবশ্যই সত্য হতে হবে অন্যথায় আমরা বাইনারি অনুসন্ধানে বাম বা ডানে পুনরাবৃত্তি/পুনরাবৃত্তি করব কিনা তা জানি না।

ইমপ্লিমেন্টেশন সম্পর্কে একটি সাইড নোট হিসাবে, index(_:offsetBy:) ফাংশন যেটি ব্যবহার করা হয়েছিল সে সম্পর্কে আরও তথ্যের জন্য, নিম্নলিখিত ডকুমেন্টেশন দেখুন।

জেনেরিক + প্রোটোকল = 💥

ডুপ্লিকেট কোড এড়াতে সঠিকভাবে ব্যবহার করলে জেনেরিক এবং প্রোটোকল একটি শক্তিশালী হাতিয়ার হতে পারে।

প্রথমত, আরেকটি টিউটোরিয়াল দেখুন, A Swift Tour , যেটি Colab বইয়ের শেষে জেনেরিকগুলিকে সংক্ষেপে কভার করে।

ধরে নিই যে জেনেরিক সম্পর্কে আপনার একটি সাধারণ ধারণা আছে, আসুন দ্রুত কিছু উন্নত ব্যবহারের দিকে নজর দেওয়া যাক।

যখন একটি একক প্রকারের একাধিক প্রয়োজনীয়তা থাকে যেমন একটি টাইপ বিভিন্ন প্রোটোকলের সাথে সামঞ্জস্যপূর্ণ, তখন আপনার হাতে বেশ কয়েকটি বিকল্প থাকে:

typealias ComparableReal = Comparable & FloatingPoint

func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
    return a > b
}

func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
    return a > b
}

func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
    return a > b
}

func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
    return a > b
}

func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
    return a > b
}

print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false
false
false
false
false

শীর্ষে typealias ব্যবহার লক্ষ্য করুন। এটি আপনার প্রোগ্রামে একটি বিদ্যমান প্রকারের নামযুক্ত উপনাম যোগ করে। একটি টাইপ উপনাম ঘোষণা করার পরে, আপনার প্রোগ্রামের সর্বত্র বিদ্যমান টাইপের পরিবর্তে উপনাম নাম ব্যবহার করা যেতে পারে। প্রকার উপনাম নতুন ধরনের তৈরি করে না; তারা কেবল একটি নামকে একটি বিদ্যমান প্রকার উল্লেখ করার অনুমতি দেয়।

এখন দেখা যাক কিভাবে আমরা প্রোটোকল এবং জেনেরিক একসাথে ব্যবহার করতে পারি।

আসুন কল্পনা করি যে আমরা একটি কম্পিউটার স্টোর যা আমরা যেকোন ল্যাপটপে বিক্রয় করি তা নির্ধারণের জন্য আমরা কীভাবে সেগুলিকে দোকানের পিছনে সংগঠিত করব:

enum Box {
    case small
    case medium
    case large
}

enum Mass {
    case light
    case medium
    case heavy
}

// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

যাইহোক, আমাদের Laptop ভর অনুসারে গোষ্ঠীবদ্ধ করার একটি নতুন প্রয়োজনীয়তা রয়েছে যেহেতু তাকগুলির ওজন সীমাবদ্ধতা রয়েছে৷

func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
    return laptops.filter { $0.mass == mass }
}

let laptops: [Laptop] = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]

যাইহোক, যদি আমরা Mass ছাড়া অন্য কিছু দ্বারা ফিল্টার করতে চাই?

একটি বিকল্প হল নিম্নলিখিতগুলি করা:

// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
    associatedtype Device
    func shouldKeep(_ item: Device) -> Bool
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
    typealias Device = Laptop
    var box: Box 

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.box == box
    }
}

struct MassFilter: DeviceFilterPredicate {
    typealias Device = Laptop  
    var mass: Mass

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.mass == mass
    }
}

// Make sure our filter conforms to `DeviceFilterPredicate` and that we are 
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
    _ laptops: [Laptop], 
    by filter: F
) -> [Laptop] where Laptop == F.Device {
    return laptops.filter { filter.shouldKeep($0) }
}

// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)]
[(c medium heavy)]

অসাধারণ! এখন আমরা যেকোনো ল্যাপটপের সীমাবদ্ধতার ভিত্তিতে ফিল্টার করতে সক্ষম। যাইহোক, আমরা শুধুমাত্র Laptop ফিল্টার করতে সক্ষম।

একটি বাক্সে আছে এবং ভর আছে যে কিছু ফিল্টার করতে সক্ষম হচ্ছে সম্পর্কে কি? হতে পারে ল্যাপটপের এই গুদামটি এমন সার্ভারের জন্যও ব্যবহার করা হবে যার একটি ভিন্ন গ্রাহক বেস রয়েছে:

// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
    var mass: Mass { get }
}

protocol Boxed {
    var box: Box { get }
}

// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

struct Server: CustomStringConvertible, Boxed, Weighable {
    var isWorking: Bool
    var name: String
    let box: Box
    let mass: Mass

    var description: String {
        if isWorking {
            return "(working \(self.name) \(self.box) \(self.mass))"
        } else {
            return "(notWorking \(self.name) \(self.box) \(self.mass))"
        }
    }
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
    var box: Box 

    func shouldKeep(_ item: T) -> Bool {
        return item.box == box
    }
}

struct MassFilter<T: Weighable>: DeviceFilterPredicate {
    var mass: Mass

    func shouldKeep(_ item: T) -> Bool {
        return item.mass == mass
    }
}

// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
    _ elements: [T], 
    by filter: F
) -> [T] where T == F.Device {
    return elements.filter { filter.shouldKeep($0) }
}


// Let's test the function out!
let servers = [
    Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
    Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
    Server(isWorking: true, name: "serverC", box: .large, mass: .light),
    Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
    Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]

let products = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))

print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)]
[(notWorking serverB medium medium)]
[(a small light)]
[(b large medium)]

আমরা এখন একটি অ্যারে ফিল্টার করতে সক্ষম হয়েছি শুধুমাত্র একটি নির্দিষ্ট struct এর কোনো প্রপার্টিই নয়, সেই সাথে সেই প্রপার্টি আছে এমন কোনো স্ট্রাকট ফিল্টার করতেও সক্ষম!

ভাল API ডিজাইনের জন্য টিপস

এই বিভাগটি WWDC 2019: Modern Swift API ডিজাইন টক থেকে নেওয়া হয়েছে।

প্রোটোকলগুলি কীভাবে আচরণ করে তা এখন আপনি বুঝতে পেরেছেন, আপনার কখন প্রোটোকল ব্যবহার করা উচিত তা বিবেচনা করা ভাল। প্রোটোকল যতটা শক্তিশালী হতে পারে, প্রোটোকল দিয়ে শুরু করা এবং অবিলম্বে শুরু করা সর্বদা সর্বোত্তম ধারণা নয়।

  • কংক্রিট ব্যবহারের ক্ষেত্রে শুরু করুন:
    • প্রথমে কংক্রিটের ধরনগুলির সাথে ব্যবহারের ক্ষেত্রে অন্বেষণ করুন এবং বুঝুন কোন কোডটি আপনি ভাগ করতে চান এবং বারবার খুঁজে বের করা হচ্ছে৷ তারপর, ফ্যাক্টর যে জেনেরিকের সাথে কোড শেয়ার করে। এর অর্থ হতে পারে নতুন প্রোটোকল তৈরি করা। জেনেরিক কোডের প্রয়োজন আবিষ্কার করুন।
  • স্ট্যান্ডার্ড লাইব্রেরিতে সংজ্ঞায়িত বিদ্যমান প্রোটোকল থেকে নতুন প্রোটোকল রচনা করার কথা বিবেচনা করুন। এটির একটি ভাল উদাহরণের জন্য নিম্নলিখিত অ্যাপল ডকুমেন্টেশন পড়ুন।
  • একটি জেনেরিক প্রোটোকলের পরিবর্তে, একটি জেনেরিক টাইপ সংজ্ঞায়িত করার কথা বিবেচনা করুন।

উদাহরণ: একটি কাস্টম ভেক্টর প্রকার সংজ্ঞায়িত করা

ধরা যাক আমরা কিছু জ্যামিতি অ্যাপে ব্যবহার করার জন্য ফ্লোটিং-পয়েন্ট নম্বরগুলিতে একটি GeometricVector প্রোটোকল সংজ্ঞায়িত করতে চাই যা আমরা তৈরি করছি যা 3টি গুরুত্বপূর্ণ ভেক্টর ক্রিয়াকলাপকে সংজ্ঞায়িত করে:

protocol GeometricVector {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

ধরা যাক আমরা ভেক্টরের মাত্রা সংরক্ষণ করতে চাই, যা SIMD প্রোটোকল আমাদের সাহায্য করতে পারে, তাই আমরা আমাদের নতুন ধরনের SIMD প্রোটোকলকে পরিমার্জিত করব। SIMD ভেক্টরগুলিকে স্থির আকারের ভেক্টর হিসাবে ভাবা যেতে পারে যেগুলি খুব দ্রুত হয় যখন আপনি ভেক্টর ক্রিয়াকলাপ সম্পাদন করতে ব্যবহার করেন:

protocol GeometricVector: SIMD {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

এখন, উপরের অপারেশনগুলির ডিফল্ট বাস্তবায়ন সংজ্ঞায়িত করা যাক:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

এবং তারপরে আমরা এই ক্ষমতাগুলি যোগ করতে চাই এমন প্রতিটি প্রকারের সাথে আমাদের একটি সামঞ্জস্য যোগ করতে হবে:

extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }

প্রোটোকল সংজ্ঞায়িত করার এই তিন-পদক্ষেপের প্রক্রিয়া, এটিকে একটি ডিফল্ট বাস্তবায়ন প্রদান করা এবং তারপর একাধিক প্রকারের সাথে সামঞ্জস্য যোগ করা মোটামুটি পুনরাবৃত্তিমূলক।

প্রোটোকল কি প্রয়োজনীয় ছিল?

SIMD প্রকারের কোনোটিরই অনন্য বাস্তবায়ন নেই তা একটি সতর্কতা চিহ্ন। সুতরাং এই ক্ষেত্রে, প্রোটোকল সত্যিই আমাদের কিছু দিচ্ছে না।

SIMD এর একটি এক্সটেনশনে এটি সংজ্ঞায়িত করা

যদি আমরা SIMD প্রোটোকলের একটি এক্সটেনশনে 3 টি অপারেটর লিখি, তাহলে এটি আরও সংক্ষিপ্তভাবে সমস্যার সমাধান করতে পারে:

extension SIMD where Scalar: FloatingPoint {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

কোডের কম লাইন ব্যবহার করে, আমরা সব ধরনের SIMD এ সমস্ত ডিফল্ট বাস্তবায়ন যোগ করেছি।

কখনও কখনও আপনি এই ধরণের শ্রেণিবিন্যাস তৈরি করতে প্রলুব্ধ হতে পারেন, তবে মনে রাখবেন যে এটি সর্বদা প্রয়োজনীয় নয়। এর মানে হল আপনার কম্পাইল করা প্রোগ্রামের বাইনারি সাইজ ছোট হবে এবং আপনার কোড কম্পাইল করা দ্রুততর হবে।

যাইহোক, এই এক্সটেনশন পদ্ধতির জন্য দুর্দান্ত যখন আপনার কাছে কয়েকটি সংখ্যক পদ্ধতি যুক্ত করতে চান। যাইহোক, আপনি যখন একটি বড় API ডিজাইন করছেন তখন এটি একটি স্কেলেবিলিটি সমস্যাকে আঘাত করে।

ইহা একটি? আছে?

আগে আমরা বলেছিলাম GeometricVector SIMD পরিমার্জন করবে। কিন্তু এটা কি একটা সম্পর্ক? সমস্যা হল যে SIMD ক্রিয়াকলাপগুলিকে সংজ্ঞায়িত করে যা আমাদের একটি ভেক্টরে একটি স্কেলার 1 যোগ করতে দেয়, কিন্তু জ্যামিতির প্রেক্ষাপটে এই ধরনের একটি অপারেশনকে সংজ্ঞায়িত করার অর্থ হয় না।

তাই, SIMD একটি নতুন জেনেরিক টাইপে মোড়ানোর মাধ্যমে একটি সম্পর্ক ভালো হতে পারে যা যেকোনো ফ্লোটিং পয়েন্ট নম্বর পরিচালনা করতে পারে:

// NOTE: `Storage` is the underlying type that is storing the values, 
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
    typealias Scalar = Storage.Scalar
    var value: Storage
    init(_ value: Storage) { self.value = value }
}

তারপরে আমরা সতর্ক হতে পারি এবং শুধুমাত্র জ্যামিতির প্রেক্ষাপটে অর্থপূর্ণ ক্রিয়াকলাপগুলিকে সংজ্ঞায়িত করতে পারি:

extension GeometricVector {
    static func + (a: Self, b: Self) -> Self {
        Self(a.value + b.value)
    }

    static func - (a: Self, b: Self) -> Self {
        Self(a.value - b.value)
    }
    static func * (a: Self, b: Scalar) -> Self {
        Self(a.value * b)
    }
}

এবং আমরা এখনও 3টি পূর্ববর্তী অপারেটর পেতে জেনেরিক এক্সটেনশন ব্যবহার করতে পারি যা আমরা বাস্তবায়ন করতে চেয়েছিলাম যা দেখতে প্রায় আগের মতোই:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a.value * b.value).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

সামগ্রিকভাবে, আমরা কেবল একটি স্ট্রাকট ব্যবহার করে আমাদের তিনটি অপারেশনের আচরণকে একটি প্রকারে পরিমার্জন করতে সক্ষম হয়েছি। প্রোটোকলের সাথে, আমরা সমস্ত SIMD ভেক্টরের পুনরাবৃত্তিমূলক কনফরমেন্স লেখার সমস্যার মুখোমুখি হয়েছি, এবং Scalar + Vector মতো নির্দিষ্ট অপারেটরগুলিকে উপলব্ধ হওয়া থেকে আটকাতে পারিনি (যা এই ক্ষেত্রে আমরা চাইনি)। যেমন, মনে রাখবেন যে প্রোটোকলগুলি সব-সকল এবং শেষ-সমস্ত সমাধান নয়। কিন্তু কখনও কখনও আরও ঐতিহ্যগত সমাধান আরও শক্তিশালী হতে পারে।

আরও প্রোটোকল-ভিত্তিক প্রোগ্রামিং সংস্থান

এখানে আলোচিত বিষয়গুলিতে অতিরিক্ত সংস্থান রয়েছে:

  • WWDC 2015: সুইফটে প্রোটোকল-ওরিয়েন্টেড প্রোগ্রামিং : এটি সুইফট 2 ব্যবহার করে উপস্থাপিত হয়েছিল, তাই তখন থেকে অনেক কিছু পরিবর্তিত হয়েছে (যেমন তারা উপস্থাপনায় ব্যবহৃত প্রোটোকলগুলির নাম) কিন্তু এটি এখনও তত্ত্বের জন্য একটি ভাল সংস্থান এবং এটির পিছনে ব্যবহার করা হয়েছে .
  • সুইফট 3-এ প্রোটোকল-ওরিয়েন্টেড প্রোগ্রামিং প্রবর্তন : এটি সুইফ্ট 3-এ লেখা হয়েছিল, তাই এটিকে সফলভাবে কম্পাইল করার জন্য কিছু কোড সংশোধন করার প্রয়োজন হতে পারে, তবে এটি আরেকটি দুর্দান্ত সংস্থান।
  • WWDC 2019: আধুনিক সুইফ্ট API ডিজাইন : মান এবং রেফারেন্স প্রকারের মধ্যে পার্থক্যগুলিকে অতিক্রম করে, যখন প্রোটোকলগুলি API ডিজাইনে খারাপ পছন্দ হিসাবে প্রমাণিত হতে পারে (উপরের "গুড এপিআই ডিজাইনের জন্য টিপস" বিভাগের মতো), কী পাথ সদস্য সন্ধান, এবং সম্পত্তি মোড়ক.
  • জেনেরিকস : সুইফট 5 এর জন্য সুইফটের নিজস্ব ডকুমেন্টেশন জেনেরিক সম্পর্কে।