Some Vs Any, Unlocking existential for all protocols (in Burmese)

Kyaw Zay Ya Lin Tun
5 min readMar 21, 2023

--

Prerequisite

  • Basic knowledge of Swift syntax
  • Generics

What I’ll cover

  • Introduction
  • Swift’s generics warm-up
  • How generic types can be replaced by ‘some’
  • Existential types warm-up
  • How existential types can be enhanced by ‘any’
  • ‘some’ Vs ‘any’ comparison

Introduction

ဒီ article မှာ အချို့ low level concept တွေကို ရှင်းပြဖို့လိုတဲ့အတွက် သက်ဆိုင်ရာ warm-up section လေးတွေ ထည့်သွင်းပေးထားပြီး အသေးစိတ်ရှင်းပြထားပါတယ်။ ဒါတွေမသိချင်ဘူး ‘some’ နဲ့ ‘any’ အကြောင်း တိုတိုနဲ့လိုရင်းပဲသိချင်တယ်ဆိုရင်တော့ ‘some’ Vs ‘any’ comparison ဆိုတဲ့ section ကို ကျော်ဖတ်ပေးပါ။

Swift’s generics warm-up

Generic ဆိုတာ ကိုယ့်ရဲ့ code block ကို type အမျိုးမျိုးထည့်ပြီး သုံးလို့ရအောင် reusable ဖြစ်အောင်လုပ်ပေးတဲ့ programming langauge feature တစ်ခုဖြစ်ပါတယ်။ Generic type တွေကို variable type, function parameter နဲ့ return type တွေအနေနဲ့ အသုံးပြုလို့ရပါတယ်။ အောက်က code example ကို ကြည့်လိုက်ရအောင်။

final class Node<T> {
var value: T
var next: Node?

init(value: T) {
self.value = value
}
}

final class LinkedList<T> {
private(set) var head: Node<T>?

var tail: Node<T>? {
guard var node = head else { return nil }
while let next = node.next { node = next }
return node
}

init() {}

init(_ node: Node<T>) {
self.head = node
}

func append(_ node: Node<T>) {
if let tail = tail {
tail.next = node
} else {
head = node
}
}

func printList() {
printAll(head)

func printAll(_ node: Node<T>?) {
guard let node = node else { return }
print("\(node.value)\(node.next == nil ? "" : " ~> ")",
terminator: node.next == nil ? "\n" : "")
printAll(node.next)
}
}
}

ရိုးရိုး Singly linked list implementation လေးတစ်ခုရေးထားပါတယ်။ ကျွန်တော်တို့ရဲ့ LinkedList မှာ head နဲ့ tail ဆိုတဲ့ Node နှစ်ခုရှိပါတယ်။ LinkedList ထဲကို element တွေအားလုံးထည့်လို့ရအောင် generic type T ကိုယူထားပါတယ်။ Int, String စတာတွေအပြင် ကိုယ့်ရဲ့ custom type တွေကိုလည်း သိမ်းထားနိုင်ပါတယ်။ ဒါ generic ရဲ့ ပုံမှန်သုံးနေကျ usecase ပါ။ ကဲ ကျွန်တော်တို့ တစ်ချက်သုံးကြည့်လိုက်ရအောင်။

let strLinkedList = LinkedList(.init(value: "🍎"))
strLinkedList.append(.init(value: "🥥"))
strLinkedList.append(.init(value: "🍉"))

strLinkedList.printList()

// Output:
🍎 ~> 🥥 ~> 🍉

မျှော်လင့်ထားသလိုပဲ ထည့်ထားတဲ့အတိုင်း 🍎, 🥥, 🍉 ဆိုပြီး ပြန်ထွက်လာပါတယ်။

How generic types can be replaced by ‘some’

Generic placeholder type တစ်ခုနေရာကို runtime မှာ concrete type တစ်ခုသာ ဝင်လို့ရပါတယ်။ Type နှစ်ခု သုံးခု ဝင်လို့မရပါဘူး။ စာနဲ့ဒီအတိုင်းရေးပြရင် ရှုပ်နေမှာစိုးလို့ strLinkedList ကိုပဲ ဥပမာယူပြီး ရှင်းပြပါမယ်။ အပေါ်က strLinkedList က linked list ထဲက generic element type T နေရာကို ဘာ type ဝင်သွားသလဲ? String type ဝယ်သွားတယ်နော် ဟုတ်တယ်ဟုတ်? ကောင်းပြီ ဒီိလိုဆိုရင် strLinkedList ထဲကို String type မှတစ်ပါး အခြား type ထပ်ဝင်လို့ရသေးသလားဆိုတော့ မရတော့ဘူး ဘယ်လိုမျိုးလဲဆိုတော့ အောက်က code လိုမျိုးပေါ့

// ✅ Valid
strLinkedList.append(.init(value: "🍋"))

// ❌ Invalid
// Cannot convert value of type 'Bool' to expected argument type 'String'
strLinkedList.append(.init(value: true))

// ❌ Invalid
// Cannot convert value of type 'Int' to expected argument type 'String'
strLinkedList.append(.init(value: 100))

LinkedList object စဆောက်စဥ်ကတည်းက constructor ထဲမှာ String type ဖြစ်တဲ့ "🍎" ကိုထည့်ခဲ့တာကြောင့် generic type Tနေရာမှာ String type ကို အလိုလို infer ဖြစ်သွားပြီးဖြစ်ပါတယ်။ ဒါကြောင့် error message မှာ expected argument type ‘String’ လို့ ပြောထားတာဖြစ်ပါတယ်။ ဒါကြောင့် generic placeholder တစ်ခုနေရာကို runtime မှာ သေချာပေါက် type တစ်ခုဝင်ရမှာဖြစ်ပြီး အဲ့ type ကိုသာ program သက်တမ်းဆုံးသည်အထိ ဆက်သုံးရမှာဖြစ်ပါတယ်။ အခြား type တစ်ခုနဲ့ replace လုပ်လို့မရပါဘူး။

some’ ကလည်း generic placeholder လိုပါဘဲ။ တစ်ခုကွာသွားတာက some ကို protocol တွေ base class တွေနဲ့သာ သုံးလို့ရပါတယ်။ သဘောတရားအရတော့ generic လိုပဲ runtime မှာ concrete type တစ်ခုသာ ဝင်လို့ရပါတယ်။ Code ရေးကြည့်လိုက်ရအောင် ကျွန်တော်တို့မှာ AnyFruit ဆိုတဲ့ protocol တစ်ခုရှိပြီး Apple, Orange နဲ့ Avocado ဆိုတဲ့ class တွေက conform လုပ်ထားတယ်ဆိုကြပါစို့။

protocol AnyFruit {
static var name: String { get }
}

struct Apple: AnyFruit {
static let name = "🍎"
}

struct Orange: AnyFruit {
static let name = "🍊"
}

struct Avocado: AnyFruit {
static let name = "🥑"
}

ပြီးရင် fruits array တစ်ခုရှိမယ်။ ဒီနေရာမှာ fruit တွေအကုန်လုံးမဟုတ်ပဲ AnyFruit ရဲ့ implementation type သုံးခုထဲကမှ Apple type ဒါမှမဟုတ် Orange type ဒါမှမဟုတ် Avocado type တစ်ခုခုသာပါတဲ့ array ဖြစ်ရပါမယ်။

// ✅ Valid
let fruits: [some AnyFruit] = [Apple(), Apple(), Apple()]

// ❌ Invalid
// Cannot convert value of type 'Orange' to expected element type 'Apple'
let fruits: [some AnyFruit] = [Apple(), Apple(), Orange()]

အပေါ်က code ကိုကြည့်လိုက်ရင် some ရဲ့ usecase ကို မြင်မယ်ထင်ပါတယ်။ Array တစ်ခုထဲမှာ Apple type နဲ့ Orangetype နှစ်ခုပါနေတာမျိုး ဖြစ်လို့မရပါဘူး။ အပေါ်က AnyFruit ကို generic type constraint အနေနဲ့သုံးထားတဲ့ function တစ်ခုရေးကြည့်မယ်ဆို အောက်ကလိုဖြစ်ပါမယ်။

// Without using `some` keyword
func doSomething<Fruit: AnyFruit>(_ fruit: Fruit) {
// Your logic here
}

အခုလိုမျိုး AnyFruit ရဲ့ concrete type ဝင်လာဖို့အတွက် placeholder အနေနဲ့သုံးတဲ့ Fruit ဆိုတဲ့ type ကို opaque type လို့ခေါ်ပြီးတော့ function parameter ထဲကို တစ်ကယ် ဝင်လာမယ့် type ကိုတော့ underlying type လို့ ခေါ်ပါတယ်။ ဒီအသုံးအနှုန်းလေးကို တစ်ချက်မှတ်ထားပါ။ ဒီ article တစ်လျှောက်မှာ opaque type နဲ့ underlying type ဆိုပြီးတော့ ဆက်သုံးသွားမှာဖြစ်ပါတယ်။

Generic placeholder ရဲ့ သဘောသဘာဝအတိုင်း runtime ရောက်တဲ့အခါ AnyFruit ရဲ့ concrete type တစ်ခုခုသာ replace လုပ်ပါလိမ့်မယ်။ ဒါကို some သုံးပြီး ပြောင်းရေးကြည့်လို့ရပါတယ်။

// Replace generic type constraint with `some` keyword
func doSomething(_ fruit: some AnyFruit) {
// Your logic here
}

Generic type constraint နဲ့ရေးတာထက်စာရင် ‘some’ keyword သုံးပြီးရေးထားတာက ဖတ်ရတာ ပိုပြီးနားလည်ရလွယ်ပါတယ်။ အပေါ်က function ကို existential type သုံးပြီးရေးမယ်ဆိုရင် အောက်ကလိုဖြစ်ပါမယ်။

// Existential AnyFruit type.
func doSomething(_ fruit: AnyFruit) {
// Your logic here
}

Existential type ထက်စာရင် generic type constraint ကိုသုံးတာက performance benefit ပိုရပါတယ်။ Existential type တွေကိုသုံးတဲ့အခါ underlying concrete implementation type တွေအားလုံးအတွက် virtual table တစ်ခုဆောက်ရပြီး runtime မှာ dynamic dispatch လုပ်ရတာမို့ CPU overhead ဖြစ်စေပါတယ်။ Existential type အကြောင်းရောက်တဲ့အခါ ဒီအကြောင်းပိုပြီး ကျယ်ကျယ်ပြန့်ပြန့်ပြောပြပါ့မယ်။

some keyword ကို SwiftUI မှာ ကျယ်ကျယ်ပြန့်ပြန့် သုံးထားပါတယ်။

struct Home: View {
var body: some View {
// Need to return one and only one concrete `View` type
}
}

ဒါပေမယ့် opaque SwiftUI view တွေမှာတော့ ViewBuilder DSL က control-flow statement တွေကနေတစ်ဆင့် underlying type ကို ပြန်ပြောင်းပေးနိုင်တာမို့ existential type ကိုသုံးနေရသလို ခံစားရမှာပါ။ ဆိုလိုချင်တာက အောက်က code က error မတက်ဘူး valid ဖြစ်တယ်လို့ ပြောတာပေါ့။

struct Home: View {
@State private var someCondition = false

var body: some View {
if someCondition {
Text("Hello World")
} else {
HStack {
Image(systemName: "house.fill")
Text("Hi!")
}
}
}
}

Existential types warm-up

Existential type ကို အပေါ်မှာလည်း အရိပ်အမြွက်တော့ ပြောထားပြီးပါပြီ။ AnyFruit ကိုဆက်သုံးပြီး example code လေးရေးကြည့်ပါမယ်။

var myFavFruit: AnyFruit = Apple()
// That's totally valid since `myFavFruit` can store any kind of `AnyFruit` subclasses
myFavFruit = Orange()

Protocol type ကို variable ရဲ့ type အနေနဲ့သုံးလိုက်တဲ့အခါ သူ့ရဲ့ implementation class တွေအားလုံး store လုပ်လို့ရပါတယ်။ ဒီလိုမျိုး concrete types တွေအားလုံး runtime မှာ dynamically replace လုပ်လို့ရတဲ့ AnyFruit ဆိုတဲ့ type ကို existential type လို့ခေါ်ပါတယ်။ ဒါက မထူးဆန်းသေးပါဘူး။ OOP ရဲ့ အခြေခံ concept တွေပါဘဲ။ ဒါပေမယ့် အဲ့လိုရေးတာက performance overhead ရှိပါတယ်။

နောက်ကွယ်မှာ AnyFruit type အနေနဲ့ store လုပ်ရမယ့် object တွေကများတဲ့အတွက် တစ်ကယ့် underlying object ကို တစ်ခြားနေရာမှာ သွားသိမ်းထားရပါတယ်။ AnyFruit အတွက် box တစ်ခုလုပ်လိုက်ပြီး(autoboxing) အဲ့ box ထဲမှာ underlying type က ဘာဖြစ်မယ်ဆိုတာကို compile time မှာ သိရမှာမဟုတ်ပါဘူး။ Runtime ရောက်တဲ့အချိန်ကျမှ virtual table ကနေ ဘယ် implementation ကိုသုံးရမလဲဆိုတာရှာပြီး variable ထဲကို store ပြန်လုပ်တာဖြစ်ပါတယ်။ အဲ့လို store လုပ်တဲ့နေရာမှာ ကျွန်တော်တို့ရဲ့ underlying object တွေသည် size သေးတာလည်းပါနိုင်သလို size ကြီးတာလည်းပါနိုင်ပါတယ်။ ဒီအတွက် ကျွန်တော်တို့ရဲ့ memory slot size သည် dynamic memory allocation ဖြစ်ပါတယ်။ ဒီလိုလုပ်တာကို dynamic dispatch လို့လဲခေါ်ပါတယ်။ အပေါ်ကပြောခဲ့တဲ့ generic type constraint နဲ့ရေးမယ်ဆိုရင်တော့ compile time မှာထဲက opaque type parameter ရဲ့ underlying type ကို တန်းသိရမှာဖြစ်တဲ့အတွက် storage size ကလည်း static ဖြစ်ပါတယ်။ ဒါကိုတော့ static dispatch လို့ခေါ်ပါတယ်။

အဲ့လိုဆိုရင် ပြောစရာရှိတာက ဒါဖြင့်လည်း static dispatch (generic type) တွေပဲသုံးတော့ပေါ့။ ဘာကြောင့် existential type တွေကို သုံးရမှာလည်းပေါ့။ OOP နဲ့ပတ်သက်တဲ့ စာအုပ်တွေတိုင်းမှာ model တွေကို design ချတဲ့အခါ protocol တွေကိုသုံးပြီး design ချတာက ပိုပြီး abstraction မြင့်ကြောင့် ပြုပြင်ပြောင်းလဲရလွယ်ကူကြောင်း ထပ်တလဲလဲရေးထားတာကိုတွေ့ရပါလိမ့်မယ်။ တစ်ခုလိုချင် နောက်တစ်ခုတော့ trade-off လုပ်ရမှာပေါ့ အခုနောက်ပိုင်း iPhone တွေက အရင်ကထက်ပိုအဆင့်မြင့်လာပြီလို့ ပြောချင်လည်းပြောလို့ရပါတယ်။

How existential types can be enhanced by ‘any’

Swift 5.7 အရောက်မှာ existential type တွေကို compile time မှာ unbox လုပ်ပေးပြီး သူ့ရဲ့ underlying type ကို replace လုပ်ပေးနိုင်တဲ့ feature တစ်ခုပေါ်ပေါက်လာပါတယ်။ အဲ့တာကတော့ ‘any’ keyword ပါဘဲ။

var myFavFruit: any AnyFruit = Apple()
// That's totally valid since `myFavFruit` can store any kind of `AnyFruit` subclasses
myFavFruit = Orange()

အပေါ်က code ကို any လေးထည့်ရေးလိုက်တာနဲ့ compile time မှာတင် unbox လုပ်ပေးပြီး runtime ကို underlying type ကိုပို့ပေးမှာပါ။ တစ်နည်းပြောရရင် dynamic dispatch ရဲ့ performance issue တွေကို ရှောင်ရှားနိုင်တော့မှာဖြစ်ပါတယ်။

‘some’ Vs ‘any’ comparison

Fixed ဖြစ်တဲ့ concrete type တစ်ခုတည်းကိုသာ store လုပ်ချင်ရင် ‘some’ ကို သုံးပါတယ်။ some က type relationship တွေကို ပိုပြီးခိုင်မာစေပါတယ်။

// ✅ Valid
var value: some Equatable = 1
// ❌ Invalid, since `value` can only store one type, i.e Int, so cannot store Double type
value = 1.0

တစ်ခုထက်ပိုတဲ့ concrete type တွေ dynamically store လုပ်ချင်ရင် ‘any’ ကိုသုံးပါတယ်။ any က type erase လုပ်ပေးတဲ့အတွက် ပိုပြီး abstract ဖြစ်သွားစေပါတယ်။

// ✅ Valid
var value: any Equatable = 1
// ✅ Still valid (Double also conforms to Equatable protocol)
value = 1.0

သီအိုရီအရတော့ default အနေနဲ့ some ကိုအမြဲသုံးရမှာဖြစ်ပြီး arbitary type တွေ store လုပ်ဖို့လိုတဲ့အခါမှသာ any ကို သုံးသင့်ပါတယ်။

--

--

Kyaw Zay Ya Lin Tun
Kyaw Zay Ya Lin Tun

Written by Kyaw Zay Ya Lin Tun

Lead iOS Dev @CodigoApps • Programming Mentor • Swift enthusiast • Community Builder • Organising CocoaHeads Myanmar 🇲🇲

No responses yet