8. Agenda
Metaprogramowanie w JS
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Czym jest metaprogramowanie?
• Metaprogramming API
• Podstawowe symbole
• Proxy
9. Czym jest metaprogramowanie?
Czyli kilka słów teorii
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
Why do JS developers wear glasses?
Because they don’t C#
10. Definicja
Czym jest metaprogramowanie?
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Program, który tworzy lub modyfikuje inny program - metaprogram
• Program, który modyfikuje samego siebie
11. Definicja
Czym jest metaprogramowanie?
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Program, który tworzy lub modyfikuje inny program - metaprogram
• Kompilatory - np. parsowanie i optymalizacja kodu
• Transpilery - np. modyfikacja kodu żeby był zgodny z urządzeniem
• IDE - np. analiza JSDoc
• Program, który modyfikuje samego siebie
• Mechanizmy refleksji
12. Definicja
Czym jest metaprogramowanie?
• Program, który tworzy lub modyfikuje inny program - metaprogram
• Kompilatory - np. parsowanie i optymalizacja kodu
• Transpilery - np. modyfikacja kodu żeby był zgodny z urządzeniem
• IDE - np. analiza JSDoc
• Program, który modyfikuje samego siebie
• Mechanizmy refleksji
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
13. Modyfikacja programu innym programem
Czym jest metaprogramowanie?
• Program, który tworzy lub modyfikuje inny program - metaprogram
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
#define true false
#define private public
Robienie zamieszania w C dzięki makrom / Zły kod nie zależy od języka
14. Programowanie refleksyjne
Czym jest metaprogramowanie?
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Introspekcja - czytanie struktury programu
• Modyfikacja - zmiana struktury programu
• Intercesja - „wchodzenie pomiędzy” - zmiana semantyki języka/programu
15. Przykłady metaprogramowania
Co możemy dzięki temu osiągnąć?
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
SQL query goes into a bar, walks up to
two tables and asks "Can I join you?"
16. Przeciążanie operatorów
Przykłady metaprogramowania
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
// Ruby
class X
attr_reader :order
def +(y)
y + 2
end
def <=>(other)
self.order <=> other.order
end
end
instance = X.new
instance + 3 // => 5
// C++
struct X {
int operator+(int y) {
return y + 2;
}
};
int main () {
X instance;
std::cout << instance + 3; // => 5
return 0;
}
18. Obsługa dynamicznych pól
Przykłady metaprogramowania
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
// PHP
class X
{
public function __get($field)
{
return $field . " value";
}
}
// Ruby
class X
def method_missing(m, *args, &block)
"#{m} value"
end
end
19. Adnotacje i dekoratory
Przykłady metaprogramowania
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
// Java
@Entity
public class X {
@Id @GeneratedValue
@Column(name = "id")
private int id;
public X() {}
}
// JavaScript
class X {
@emitOnEnd("someEvent")
doSomething () {
console.log("Something there")
}
}
21. Imitowanie tablicy
Przykłady metaprogramowania
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
// PHP
class X implements ArrayAccess
{
public function offsetExists ($o) {}
public function offsetGet ($o) {}
public function offsetSet ($o, $v) {}
public function offsetUnset ($o) {}
}
$instance = new X();
$instance["key"];
$instance[0];
// JavaScript
// Array-like object
const a = {
0: 1,
1: 4943,
2: 2213,
length: 3
}
a[0];
22. Przeciążanie metod/klas
Przykłady metaprogramowania
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
// Ruby
class X
def something
'hello!'
end
end
class X
def somethingElse
'hello 2!'
end
end
instance = X.new
instance.print // => "hello!"
instance.printSomething // => "hello 2!"
// JavaScript
// Works pretty good!
function X () {}
X.prototype.something = function () {
return "hello!"
}
X.prototype.somethingElse = function () {
return "hello 2!"
}
instance = new X()
instance.print() // => "hello!"
instance.printSomething() // => "hello 2!”
23. Metaprogramming API
Podstawowe możliwości w JavaScript
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
I've got a really good UDP joke to tell you,
but I don't know if you'll get it
24. Na co nam pozwala podstawowe API?
Metaprogramming API
• Analiza i modyfikacja struktury obiektów
• Czytanie informacji o funkcjach
• Zarządzanie łańcuchem prototypów
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
25. Analiza i modyfikacja obiektów
Metaprogramming API
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Czytanie podstawowej struktury obiektów
• Zarządzanie stałością obiektów (mutability)
• Łańcuch prototypów
Object.getPrototypeOf(obj)
Object.setPrototypeOf(obj, null)
Object.hasOwnProperty("key") // => true Object.getOwnPropertyNames(obj)
Object.keys(obj) // => [ "key" ]
Object.preventExtensions(obj) Object.isExtensible(obj) // => false
Object.seal(obj) Object.isSealed(obj) // => true
Object.freeze(obj) Object.isFrozen(obj) // => true
26. Funkcje
Metaprogramming API
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Wykonywanie funkcji
• Czytanie struktury funkcji
• Tworzenie funkcji
Math.max.call(Math, 10, 20) // => 20
Math.max.apply(Math, [ 10, 20 ]) // => 20
const func = Array.prototype.concat.bind([ 10, 30 ], 20)
func(40) // => [ 10, 30, 20, 40 ]
function test (a, b) {} test.length // => 2
test.toString() // => "function test (a, b) {}"
function test2 (a, ...args) {} test2.length // => 1
const sum = new Function("a", "b", "return a + b")
sum(10, 20) // => 30
27. Reflect API
Metaprogramming API
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
// Function options wrappers
Reflect.apply(Math.max, null, [ 5, 15 ])
Reflect.construct(X, [ "John" ])
// Language wrappers
Reflect.has(…) // „in” operator
Reflect.deleteProperty(…) // „delete”
Reflect.get(…) // access property
Reflect.set(…) // set property value
// Object methods wrappers
Reflect.defineProperty(…)
Reflect.getOwnPropertyDescriptor(…)
Reflect.ownKeys(…)
Reflect.preventExtensions(…)
Reflect.isExtensible(…)
Reflect.getPrototypeOf(…)
Reflect.setPrototypeOf(…)
• Elastyczniejsze API do wielu metod:
• API funkcji
• Operacje języka
• Refleksja obiektów
31. Dekoratory klas
Metaprogramming API
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
function Serializable (Cls) {
Cls.prototype.toString = function () {
const str = JSON.stringify(this)
return Cls.name + ": " + str
}
}
@Serializable
class Point {
constructor (x, y) {
this.x = x
this.y = y
}
}
const point = new Point(1, 5)
console.log(point.toString())
// "Point: {"x":1,"y":5}"
• W klasie dekoratory mogą nadpisać klasę
lub edytować jej prototyp w dowolny sposób
32. Dekoratory klas: przykład
Metaprogramming API
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
@Entity()
class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
age: number;
}
• ActiveRecord / ORM
• Przykład z TypeORM
https://github.com/typeorm/typeorm
33. Podstawowe symbole
Zmiana semantyki działania programu
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
Why do programmers confuse
Halloween with Christmas?
Because OCT 31 = DEC 25.
34. Czym są symbole?
Podstawowe symbole
// Declare standard symbol
const internal = Symbol()
const symbol = Symbol()
symbol == internal // => false
• Używane jako unikalna wartość
• Nie kolidują z innymi typami
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
35. Czym są symbole?
Podstawowe symbole
// Declare standard symbol
const internal = Symbol()
const symbol = Symbol()
symbol == internal // => false
// Prepare example object
const object = {}
// Set up some value in object
object[internal] = "value"
object[internal] + "s" // => "values"
Object.keys(object) // => []
• Używane jako unikalna wartość
• Nie kolidują z innymi typami
• Mogą być używane jako klucz obiektu
• Ukryte w obiekcie
poza getOwnPropertySymbols i Reflect.ownKeys
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
36. Czym są symbole?
Podstawowe symbole
// Declare standard symbol
const internal = Symbol.for("debug")
const symbol = Symbol.for("debug")
internal == symbol // => true
Symbol.keyFor(internal) // => „debug"
• Mogą mieć swój identyfikator
• Do pobrania przez Symbol.keyFor
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
37. Well-known symbols
Podstawowe symbole
class Something {
// getter in constructor
static get [Symbol.isMagical] () {
return true
}
// getter for instance
get [Symbol.isMagicDone] () {
return "Magic has been done"
}
// method for instance
[Symbol.makeMagic] () {
// Poof!
}
}
• Dostępne w przestrzeni Symbol
• Przykłady Symbol.iterator, Symbol.species
• Przypisane do prototypu lub bezpośrednio
do konstruktora
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
38. Symbol.iterator
Podstawowe symbole
class Range {
constructor (from, to) {
this.from = from
this.to = to
}
*[Symbol.iterator] () {
let i = this.from
while (i <= this.to) {
yield i
i++
}
}
}
const range = [ ...new Range(1, 5) ]
console.log(range) // [ 1, 2, 3, 4, 5 ]
• Pozwala na implementację iteratora
• for…of loop
• spread operator - [ …arr ]
• Array.from
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
39. Symbol.toPrimitive
Podstawowe symbole
const person = {
name: "John Doe",
[Symbol.toPrimitive] (hint) {
if (hint === "string") {
return this.name
} else if (hint === "number") {
return this.name.length
}
// "default"
return "Hi, I am " + this.name
}
}
`${person}` // => "John Doe"
+person // => 8
person + "" // => "Hi, I am John Doe"
person + 3 // => "Hi, I am John Doe3"
• Zmiana zachowania przy zrzucaniu do typu
• Rozróżnia prymitywne typy
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
40. Symbol.hasInstance
Podstawowe symbole
const notNode = x => !(x instanceof Node)
class Node {}
class NodeList extends Array {
static get [Symbol.hasInstance] (o) {
const proto = Object.getPrototypeOf(o)
const c = proto.constructor
if (c === NodeList) { return true }
if (!Array.isArray(o)) { return false }
return o.findIndex(notNode) === -1
}
}
const node = new Node()
[ 1 ] instanceof NodeList // => false
[ node ] instanceof NodeList // => true
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Definiuje działanie instanceof
• Sprawdza czy obiekt jest „instancją” klasy
• Sugar syntax:
obj instanceof Iterable wygląda lepiej niż:
isIterable(obj) czy Symbol.iterator in obj
41. Symbol.species
Podstawowe symbole
// Standard behavior
class TimeoutPromise extends Promise {}
const self = x => x
const promise = new TimeoutPromise(self)
promise.then(x => x) // => TimeoutPromise
// Modified behavior
class TimeoutPromise extends Promise {
static get [Symbol.species] () {
return Promise
}
}
const self = x => x
const promise = new TimeoutPromise(self)
promise.then(x => x) // => Promise
• Zmienia zwracany typ z podstawowych metod
• Przydatny jeśli chcemy przejść do standardu
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
42. Symbol.isConcatSpreadable
Podstawowe symbole
// Standard behavior
const o = {
0: "a",
1: "b",
length: 2
}
[ 1 ].concat(o) // => [ 1,{0:"a",1:"b"} ]
// Modified behavior
const o = {
0: "a",
1: "b",
length: 2,
[Symbol.isConcatSpreadable]: true
}
[ 1 ].concat(o) // => [ 1, "a", "b" ]
• Definiuje zachowanie funkcji concat
• Decyduje czy obiekt ma być spłaszczony
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
43. Symbol.match, replace, split i search
Podstawowe symbole
const str = "<strong>Success</strong>"
class Tag {
constructor (name) {
this.name = name
}
[Symbol.search] (str) {
return str.indexOf(
"<" + this.name + ">"
)
}
}
str.search(new Tag("strong")) // => 0
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
• Definiują działanie obiektu w kilku metodach
• string.split(object)
• string.search(object)
• string.replace(object, replacement)
• string.match(object)
44. Symbol.toStringTag
Podstawowe symbole
// Standard behavior
const object = {}
"" + object // => "[object Object]"
// Modified behavior
const object = {
[Symbol.toStringTag]: "X"
}
"" + object // => "[object X]"
• Definiuje podstawowy tag nazwy obiektu
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
45. Symbol.unscopables
Podstawowe symbole
// Declare object
const object = {
txt1: "value",
txt2: "value2",
[Symbol.unscopables]: { "txt2": true }
}
with (object) {
console.log(txt1) // => "value"
console.log(txt2) // ReferenceError
}
• Blokuje pola dla konstrukcji with {}
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
47. Jak działa proxy?
Proxy
function SomeObject () {
this.pre = "Something "
}
const object = new SomeObject()
const proxy = new Proxy(object, {
get (target, property) {
return target.pre + property
}
})
proxy.special // "Something special"
proxy.funny // "Something funny"
Object.getPrototypeOf(proxy).constructor
// SomeObject
• Ustawia pułapki („traps”) na akcje
• Pozwala dynamicznie na zmiany zachowania
• Ma dostęp do prototypu i konstruktora
• Ma dostęp do pól
• Niezmienione akcje zachowują się tak samo
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
48. Gdzie możemy postawić „pułapki”?
Proxy
• Zmiana i pobranie parametru
• Konstrukcja obiektu, wykonanie funkcji
• Zmiana i pobranie prototypu
• Operatory in, delete
• Pobranie kluczy obiektu
• Więcej: http://bit.ly/mdnproxy
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
function Something () {
this.pre = "Something "
}
const object = new X()
const proxy = new Proxy(object, {
get (target, property) {
return target.pre + property
},
set (target, property, value) {
throw new Error("You can't change!")
}
})
Object.getPrototypeOf(proxy) // Something
proxy.special // "Something special"
proxy.special = "xyz" // Error
49. Przykład: Dependency Injection
Proxy
• Po prawej szkic DI
• brak obsługi błędów
• brak poprawnego cache
• niezoptymalizowane
• nie działa poprawnie „in” czy „getKeys”
• Dynamiczne przypisanie danych
• Możliwość "lazy loadingu"
• Przykład: github.com/rangoo94/easen-di
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
function Di (dependencies = {}) {
const cache = {}
this.proxy = new Proxy(dependencies, {
set (deps, prop, value) {
if (typeof value !== "function") {
throw new TypeError()
}
deps[prop] = value
},
get (deps, prop) {
if (!deps[prop]) {
throw new ReferenceError()
}
if (!cache[prop]) {
cache[prop] = deps[prop]()
}
return cache[prop]
}
})
}
const di = new Di({ x: () => "Hello" })
di.proxy.x // "Hello"
50. Przykład: Automatyczna walidacja na osobnej warstwie
Proxy
• Walidacja niezależna od obiektu końcowego
• Możemy mieć jeden obiekt, ale N walidacji
• Walidacja zależna od miejsca zmiany
• Przykład - jeden model User zamiast:
• CreatedUser
• PostUser
• AdminUser
• RegularUser
• StaffUser
• SomeWeirdUser
• …
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
function Point (x, y) {
this.x = x
this.y = y
}
function assertNumber (x) {
if (typeof x !== "number") {
throw new Error("Incorrect number")
}
}
const checks = {
x: assertNumber,
y: assertNumber
}
function createPoint (...args) {
return new Proxy(new Point(...args), {
set (target, key, value) {
const v = checks[key] || (x => x)
v(value, target)
target[key] = value
}
})
}
51. Przykład: Prywatne wartości
Proxy
• Brak dostępu do wartości
• Bez użycia domknięć
• http://bit.ly/privatejs
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
function User (name, password) {
this.name = name
this.password = password
}
User.prototype.login = function () {
console.log(this.name, this.password)
}
function createUser (...args) {
const priv = [ "password" ]
return new Proxy(new User(...args), {
get (target, key) {
return priv.includes(key) ?
undefined : target[key]
},
ownKeys (target) {
return Reflect.ownKeys(target)
.filter(key => !priv.includes(key))
}
})
}
const user = createUser("name", "pass")
Object.keys(user) // [ "name" ]
user.login() // "name" undefined
52. Przykład: Prywatne wartości
Proxy
• Brak dostępu do wartości
• Bez użycia domknięć
• „this” kieruje na proxy, jako metoda proxy
• http://bit.ly/privatejs
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
User.prototype.login = function () {
console.log(this.name, this.password)
}
get (target, key) {
return priv.includes(key) ?
undefined : target[key]
}
function User (name, password) {
this.name = name
this.password = password
}
User.prototype.login = function () {
console.log(this.name, this.password)
}
function createUser (...args) {
const priv = [ "password" ]
return new Proxy(new User(...args), {
get (target, key) {
return priv.includes(key) ?
undefined : target[key]
},
ownKeys (target) {
return Reflect.ownKeys(target)
.filter(key => !priv.includes(key))
}
})
}
const user = createUser("name", "pass")
Object.keys(user) // [ "name" ]
user.login() // "name" undefined
53. Przykład: Prywatne wartości
Proxy
• Brak dostępu do wartości
• Bez użycia domknięć
• „this” kieruje na proxy, jako metoda proxy
• http://bit.ly/privatejs
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
function User (name, password) {
this.name = name
this.password = password
}
/* User.prototype.login as before */
function createUser (...args) {
const priv = [ "password" ]
return new Proxy(new User(...args), {
get (t, key) {
if (typeof t[key] === "function") {
return (...args) => {
return t[key](...args)
}
}
return priv.includes(key) ?
undefined : t[key]
},
ownKeys (target) {
return Reflect.ownKeys(target)
.filter(key => !priv.includes(key))
}
})
}
54. Przykład: QueryBuilder/DSL
Proxy
• Dynamiczne łańcuchy funkcjonalności
Dawid Rusnak
www.drcode.pl / GitHub: @rangoo94
function QueryBuilder () {
return new Proxy([], {
get (parts, prop, proxy) {
if (prop === "value") {
return parts.join(" ")
}
prop = prop.toUpperCase()
return arg => {
parts.push(`${prop} ${arg}`)
return proxy
}
}
})
}
new QueryBuilder()
.select("*")
.from("articles")
.where("user_id = 10")
.and("visible = true")
.value
// "SELECT * FROM articles
WHERE user_id = 10 AND visible = true"