Swift kontra Javascript

Pytanie z tematu tego wpisu jest mocno podchwytliwe 😁. Wiadomo, Swift to język typowany i to na dodatek typowany bezpiecznie (type-safe) w porównaniu do Javascript’u, który jest interpretowany i typowany dynamicznie. Co zatem łączy oba te języki? 🤔 Jeśli mam twoją ciekawość, to zapraszam do dalszej części tekstu 🚀.

Dynamic Member Lookup

Z pozoru może się wydawać, że Swift’owi daleko jest do języków potocznie nazywanych „skryptowymi”, takich jak wspomniany wcześniej JS, ale również PHP czy Python. Porównując je ze sobą mi od razu nasuwają się dwie myśli. Pierwsza to skojarzenie Swift’a z bezpieczeństwem, tutaj chyba tematu nie trzeba zbytnio rozwijać 😁. Zaś drugie to sposób, w jaki języki skryptowe podchodzą do składni samej w sobie. Tutaj świetnym przykładek jest JS, który jako język interpretowany nie uznaje za zło np. próby wywołania zmiennej na obiekcie, która to zmienna nie istnieje. Dla jednych to totalnie niezrozumiałe zachowanie, dla z drugiej strony wprowadzą lekką dozę „relaksu”, wybacza więcej błędów, jest po prostu… bardziej user-friendly 😁. Czemu zatem nie użyć również takiego podejścia… w Swift’cie, ale z zachowaniem jest bezpiecznego typowania.

Z pomocą przychodzi zmiana SE-019 (wraz z wersją 4.2 języka) wprowadzająca atrybut @dynamicMemberLookup. Instruuje ona Swift’a aby wołana była metoda subscript, kiedy na obiekcie odwołujemy się do jakiejś zmiennej. Jeżeli jednak zmienna nie istnieje, to subscript zwróci nam wartość domyślą określonego typu – zamiast undefined znanego z JS’a.

Wymaganiem wspomnianego atrybutu jest implementacja wspomnianej już metody subscript(dynamicMember:). Weźmy pierwsze z brzegu przykład, strukturę Car:

@dynamicMemberLookup
struct Car {
    subscript(dynamicMember member: String) -> String {
        let properties = ["brand": "Ferrari 🎠"]
        return properties[member, default: "-"]
    }
}

Na górze mamy atrybut dynamicMemberLookup, zaś w naszej strukturze implementujemy wymaganą metodą subscript. Definicja metody musi być identyczna, inaczej prekompilator nie puści nas dalej. Teraz możemy stworzyć obiekt typu Car i spróbować odpytać o dowolną zmienną. Jeśli zmienna nie istnieje w naszym słowniku properties wewnątrz metody subscript, to metoda zwróci wartość domyślną – w naszym przypadku ”-” :

let car = Car()
print(car.brand)   // "Ferrari 🎠"
print(car.dummy)   // "-" 

No dobrze, ale teraz nasuwają się nam dwa pytanie:

  1. Co to ma wspólnego z tym „type-safe”? Gdzie ono jest zachowane?
  2. Co w przypadku, jak chciałbym mieć więcej typów zwracanych, niż tylko String w tym przypadku?

Śpieszę z odpowiedzią. Ponieważ zawsze chcemy otrzymać jakąś wartość domyślną, to odpadają generyki. Ale przecież nic nie stoi na przeszkodzie, żeby dodać kolejną metodą subscript, ale dla innego typu:

@dynamicMemberLookup
struct Car {
    subscript(dynamicMember member: String) -> String {
        let properties = ["brand": "Ferrari 🎠 "]
        return properties[member, default: "-"]
    }
    subscript(dynamicMember member: String) -> Int {
        let properties = ["production": 2012]
        return properties[member, default: 0]
    }
}

Tym razem przy wołaniu zmiennej, musimy jawnie wskazać kompilatorowi, który typ chcemy użyć. Tutaj właśnie jest nasze bezpieczeństwo typów, gdyż wiemy że nawet w przypadku zawołania zmiennej, której nie ma – nie dostaniemy nil’a 😁.

@dynamicMemberLookup
struct Car {
    subscript(dynamicMember member: String) -> String {
        let properties = ["brand": "Ferrari 🎠 "]
        return properties[member, default: "-"]
    }
    subscript(dynamicMember member: String) -> Int {
        let properties = ["production": 2012]
        return properties[member, default: 0]
    }
}
    
let car = Car()
let brand: String = car.brand
let productionYear: Int = car.production
print("Car \(brand) produced in \(productionYear)")
// Car Ferrari 🎠 produced in 2012

Tadam!

Co więcej – nie musimy się ograniczać tylko do ścisłych typów. W końcu to Swift, możemy też zwracać closures:

@dynamicMemberLookup
struct Car {
    subscript(dynamicMember member: String) -> String {
        let properties = ["brand": "Ferrari 🎠 "]
        return properties[member, default: "-"]
    }
    subscript(dynamicMember member: String) -> Int {
        let properties = ["production": 2012]
        return properties[member, default: 0]
    }
    subscript(dynamicMember member: String) -> (_ input: String) -> Void {
        return {
            print("I'm doing something with \($0).")
        }
    }
}
        
let car = Car()
car.doSomethingWith("It") // I'm doing something with It.

I to działa!

W praktyce świetne zastosowanie podał sam Cris Latner w przypadku pracy z plikami typu JSON:

@dynamicMemberLookup
enum JSON {
    case intValue(Int)
    case stringValue(String)
    case arrayValue(ArrayopenTagJSON>)
    case dictionaryValue(DictionaryopenTagString, JSON>)
    
    var stringValue: String? {
        if case .stringValue(let str) = self {
            return str
        }
        return nil
    }
    
    subscript(index: Int) -> JSON? {
        if case .arrayValue(let arr) = self {
            return index openTag arr.count ? arr[index] : nil
        }
        return nil
    }
    
    subscript(key: String) -> JSON? {
        if case .dictionaryValue(let dict) = self {
            return dict[key]
        }
        return nil
    }
    
    subscript(dynamicMember member: String) -> JSON? {
        if case .dictionaryValue(let dict) = self {
            return dict[member]
        }
        return nil
    }
}

Dynamic Callable

Ale na dynamic members nie koniec. W ramach ewolucji języka do wersji 5.0 zmiana SE-0216 idzie dalej, przynosząc atrybut @dynamicCallable. Jest to niejako rozszerzenie dynamic Members, które w naturalny sposób pozwala językowi Swift kooperować z innymi językami skryptowymi. Nie będę jednak tej zmiany bardzo mocno rozpisywał, gdyż należy ją traktować jako swoisty „syntactic sugar”, a niżeli kolejną magię kompilatora 😅.

Przykład na dopełnienie tej dobroci:

@dynamicCallable
struct Random {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairsopenTagString, Int>) -> Double {
        let zeros = Double(args.first?.value ?? 0)
        let maximum = pow(10, zeros)
        return Double.random(in: 0...maximum)
    }
}
    
let random = Random()
print(random(of: 3))
print(random(value: 3))
print(random(3))

Generator licz losowych, w których jako parametr metoda przyjmuje rząd wielkości, do jakiej może losować liczbę. Warto tutaj zwrócić uwagę na totalną dowolność z jaką możemy przekazać parametr do metody. Niezłe, co 😅?

Podsumowanie

Zatem wracając do clickbajtowego pytania z nagłówka: Czy Swift to nowy Javascript? Odpowiedź jest… to zależy 😁. Na pewno Dzięki wprowadzeniu atrybutu dynamic member oraz dynamic calllable Swift może niejako pracować w trybie języków skryptowych. To napewno bardzo pomocne urozmaicenie dla osób przesiadających się z takich języków skryptowych jak PHP czy Python, do tego z całym dobrodziejstwem bezpiecznego typowania! Natomiast nie uważam żeby ta zmiana to był jakiś killer feature. Ale dobrze wiedzieć, że takie cuda tutaj tez umiemy 😎.

Czy Swift to nowy Javascript?

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *