Slide 1

Slide 1 text

CÓDIGO QUE GENERA CÓDIGO para hacernos la vida más fácil

Slide 2

Slide 2 text

GUIDO MARUCCI BLAS Co-founder at Wolox ▸ @guidomb ▸ guidomb ▸ https://guidomb.blog/

Slide 3

Slide 3 text

AGENDA ▸ ¿Qué es metaprogramming? ▸ Introducción a Sourcery ▸ Sourcery, virtual views y algoritmo de diff

Slide 4

Slide 4 text

¿QUÉ ES METAPROGRAMMING?

Slide 5

Slide 5 text

CÓDIGO QUE GENERA CÓDIGO

Slide 6

Slide 6 text

Metaprogramming is a programming technique in which computer programs have the ability to treat programs as their data. It means that a program can be designed to read, generate, analyse or transform other programs, and even modify itself while running.

Slide 7

Slide 7 text

▸ Trata a otros programas como datos de entrada o salida ▸ Pueden diseñarse para leer, generar, analizar o transformar otros programas ▸ Se puede modificar a si mismo mientras corre

Slide 8

Slide 8 text

HAY DISTINTAS FORMAS DE HACER METAPROGRAMACIÓN

Slide 9

Slide 9 text

▸ En tiempo de compilación (C, Rust, Scala) ▸ En tiempo de ejecución (Java, Ruby)

Slide 10

Slide 10 text

SOURCERY ▸ Sourcery is a code generator for Swift language, built on top of Apple's own SourceKit. ▸ https://github.com/krzysztofzablocki/Sourcery

Slide 11

Slide 11 text

OBJETIVO Poder listar todos los posibles casos de un enum y saber cuantos casos tiene un enum.

Slide 12

Slide 12 text

protocol AutoCases { }

Slide 13

Slide 13 text

{% for enum in types.implementing.AutoCases|enum %} extension {{ enum.name }} { static let count: Int = {{ enum.cases.count }} {% if not enum.hasAssociatedValues %} static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} {% endfor %}] {% endif %} } {% endfor %}

Slide 14

Slide 14 text

{% for enum in types.implementing.AutoCases|enum %} extension {{ enum.name }} { static let count: Int = {{ enum.cases.count }} {% if not enum.hasAssociatedValues %} static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} {% endfor %}] {% endif %} } {% endfor %}

Slide 15

Slide 15 text

{% for enum in types.implementing.AutoCases|enum %} extension {{ enum.name }} { static let count: Int = {{ enum.cases.count }} {% if not enum.hasAssociatedValues %} static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} {% endfor %}] {% endif %} } {% endfor %}

Slide 16

Slide 16 text

{% for enum in types.implementing.AutoCases|enum %} extension {{ enum.name }} { static let count: Int = {{ enum.cases.count }} {% if not enum.hasAssociatedValues %} static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} {% endfor %}] {% endif %} } {% endfor %}

Slide 17

Slide 17 text

{% for enum in types.implementing.AutoCases|enum %} extension {{ enum.name }} { static let count: Int = {{ enum.cases.count }} {% if not enum.hasAssociatedValues %} static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} {% endfor %}] {% endif %} } {% endfor %}

Slide 18

Slide 18 text

{% for enum in types.implementing.AutoCases|enum %} extension {{ enum.name }} { static let count: Int = {{ enum.cases.count }} {% if not enum.hasAssociatedValues %} static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} {% endfor %}] {% endif %} } {% endfor %}

Slide 19

Slide 19 text

{% for enum in types.implementing.AutoCases|enum %} extension {{ enum.name }} { static let count: Int = {{ enum.cases.count }} {% if not enum.hasAssociatedValues %} static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} {% endfor %}] {% endif %} } {% endfor %}

Slide 20

Slide 20 text

enum HTTPMethod: AutoCases { case post case get case head case put case delete }

Slide 21

Slide 21 text

brew install sourcery cd MyProject sourcery --templates ./Templates \ --output ./Sources/AutoGenerated \ --sources ./Sources

Slide 22

Slide 22 text

extension HTTPMethod { static let count: Int = 5 static let allCases: [HTTPMethod] = [ .post, .get, .head, .put, .delete ] }

Slide 23

Slide 23 text

OBJETIVO Convertir automaticamente las CodingKeys de un enconder de snake case a camel case (No es más necesario a partir de Swift 4.1 gracias a .keyDecodingStrategy)*

Slide 24

Slide 24 text

"user": { "login": "baxterthehacker", "id": 6752317, "avatar_url": "https://avatars...", "gravatar_id": "", "url": "https://api.github.com/users/baxterthehacker", "gists_url": "https://api.github.com/users/...", "starred_url": "https://api.github.com/...", "organizations_url": "https://api.github.com/...", "repos_url": "https://api.github.com/...", "type": "User", "site_admin": false }

Slide 25

Slide 25 text

public struct GitHubUser: Decodable, AutoCodingKeys { public let login: String public let id: UInt public let avatarUrl: URL public let gravatarId: String public let url: URL public let gistsUrl: URL public let starredUrl: URL public let organizationsUrl: URL public let reposUrl: URL public let type: String public let siteAdmin: Bool }

Slide 26

Slide 26 text

public struct GitHubUser: Decodable, AutoCodingKeys { public let login: String public let id: UInt public let avatarUrl: URL public let gravatarId: String public let url: URL public let gistsUrl: URL public let starredUrl: URL public let organizationsUrl: URL public let reposUrl: URL public let type: String public let siteAdmin: Bool }

Slide 27

Slide 27 text

{% for type in types.implementing.AutoCodingKeys %} extension {{ type.name }} { enum CodingKeys : String, CodingKey { {% for instanceVariable in type.instanceVariables %} case {{ instanceVariable.name }} = "{{ instanceVariable.name|camelToSnakeCase }}" {% endfor %} } } {% endfor %}

Slide 28

Slide 28 text

{% for type in types.implementing.AutoCodingKeys %} extension {{ type.name }} { enum CodingKeys : String, CodingKey { {% for instanceVariable in type.instanceVariables %} case {{ instanceVariable.name }} = "{{ instanceVariable.name|camelToSnakeCase }}" {% endfor %} } } {% endfor %}

Slide 29

Slide 29 text

{% for type in types.implementing.AutoCodingKeys %} extension {{ type.name }} { enum CodingKeys : String, CodingKey { {% for instanceVariable in type.instanceVariables %} case {{ instanceVariable.name }} = "{{ instanceVariable.name|camelToSnakeCase }}" {% endfor %} } } {% endfor %}

Slide 30

Slide 30 text

{% for type in types.implementing.AutoCodingKeys %} extension {{ type.name }} { enum CodingKeys : String, CodingKey { {% for instanceVariable in type.instanceVariables %} case {{ instanceVariable.name }} = "{{ instanceVariable.name|camelToSnakeCase }}" {% endfor %} } } {% endfor %}

Slide 31

Slide 31 text

{% for type in types.implementing.AutoCodingKeys %} extension {{ type.name }} { enum CodingKeys : String, CodingKey { {% for instanceVariable in type.instanceVariables %} case {{ instanceVariable.name }} = "{{ instanceVariable.name|camelToSnakeCase }}" {% endfor %} } } {% endfor %}

Slide 32

Slide 32 text

extension GitHubUser { enum CodingKeys : String, CodingKey { case login = "login" case id = "id" case avatarUrl = "avatar_url" case gravatarId = "gravatar_id" case url = "url" case gistsUrl = "gists_url" case starredUrl = "starred_url" case reposUrl = "repos_url" case type = "type" case siteAdmin = "site_admin" } }

Slide 33

Slide 33 text

extension GitHubUser { enum CodingKeys : String, CodingKey { case login = "login" case id = "id" case avatarUrl = "avatar_url" case gravatarId = "gravatar_id" case url = "url" case gistsUrl = "gists_url" case starredUrl = "starred_url" case reposUrl = "repos_url" case type = "type" case siteAdmin = "site_admin" } }

Slide 34

Slide 34 text

SOURCERY ▸ Evitamos trabajo repetitivo ▸ Evitamos bugs debido a olvidos o problemas de sincronización entre la "fuente de verdad" y el código ▸ No incurrimos en mayores riesgos al incluir Sourcery como dependencia, termina generando código Swift.

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

PORTAL A (potentially) cross-platform, unidirectional data flow framework to build applications using a declarative and immutable UI API.

Slide 37

Slide 37 text

PORTAL ▸ github.com/guidomb/Portal ▸ Implementación de la arquitectura de Elm (o React/Redux) en Swift ▸ Incluye virtual views, diffing algorithm y manejo de estado

Slide 38

Slide 38 text

RENDERING PIPELINE

Slide 39

Slide 39 text

1. La aplicación genera una jerarquía virtual de vistas para el estado actual de la aplicación 2. Portal compara la vieja jerarquía virtual de vistas con la recientemente generada 3. Portal aplica los cambios solo a los nodos que fueron actualizados (objetos UIView)

Slide 40

Slide 40 text

1. La aplicación genera una jerarquía virtual de vistas para el estado actual de la aplicación 2. Portal compara la vieja jerarquía virtual de vistas con la recientemente generada 3. Portal aplica los cambios solo a los nodos que fueron actualizados (objetos UIView)

Slide 41

Slide 41 text

1. La aplicación genera una jerarquía virtual de vistas para el estado actual de la aplicación 2. Portal compara la vieja jerarquía virtual de vistas con la recientemente generada 3. Portal aplica los cambios solo a los nodos que fueron actualizados (objetos UIView)

Slide 42

Slide 42 text

let component: Component = container( children: [ label( text: "Hello Portal!", style: labelStyleSheet() { base, label in base.backgroundColor = .white label.textColor = .red label.textSize = 12 }, layout: layout() { $0.flex = flex() { $0.grow = .one } $0.justifyContent = .flexEnd } ) button( properties: properties() { $0.text = "Tap to like!" $0.onTap = .like } ) button( properties: properties() { $0.text = "Tap to go to detail screen" $0.onTap = .goToDetailScreen } ) ] )

Slide 43

Slide 43 text

button( properties: properties() { $0.text = "Tap to go to detail screen" $0.onTap = .goToDetailScreen } )

Slide 44

Slide 44 text

label( text: "Hello Portal!", style: labelStyleSheet() { base, label in base.backgroundColor = .white label.textColor = .red label.textSize = 12 }, layout: layout() { $0.flex = flex() { $0.grow = .one } $0.justifyContent = .flexEnd } )

Slide 45

Slide 45 text

label( text: "Hello Portal!", style: labelStyleSheet() { base, label in base.backgroundColor = .white label.textColor = .red label.textSize = 12 }, layout: layout() { $0.flex = flex() { $0.grow = .one } $0.justifyContent = .flexEnd } )

Slide 46

Slide 46 text

label( text: "Hello Portal!", style: labelStyleSheet() { base, label in base.backgroundColor = .white label.textColor = .red label.textSize = 12 }, layout: layout() { $0.flex = flex() { $0.grow = .one } $0.justifyContent = .flexEnd } )

Slide 47

Slide 47 text

label( text: "Hello Portal!", style: labelStyleSheet() { base, label in base.backgroundColor = .white label.textColor = .red label.textSize = 12 }, layout: layout() { $0.flex = flex() { $0.grow = .one } $0.justifyContent = .flexEnd } )

Slide 48

Slide 48 text

public indirect enum Component { case button(ButtonProperties, StyleSheet, Layout) case label(LabelProperties, StyleSheet, Layout) case mapView(MapProperties, StyleSheet, Layout) case imageView(Image, StyleSheet, Layout) case container([Component], StyleSheet, Layout) case table(TableProperties, StyleSheet, Layout) case collection(CollectionProperties, StyleSheet, Layout) case carousel(CarouselProperties, StyleSheet, Layout) case touchable(gesture: Gesture, child: Component) case segmented(ZipList>, StyleSheet, Layout) case progress(ProgressCounter, StyleSheet, Layout) case textField(TextFieldProperties, StyleSheet, Layout) case custom(CustomComponent, StyleSheet, Layout) case spinner(StyleSheet, Layout) case textView(TextViewProperties, StyleSheet, Layout) case toggle(ToggleProperties, StyleSheet, Layout) }

Slide 49

Slide 49 text

diff(old: Component, new: Component) ! ChangeSet

Slide 50

Slide 50 text

apply(changeSet: ChangeSet, to: UIView)

Slide 51

Slide 51 text

Component.button( ButtonProperties, StyleSheet, Layout )

Slide 52

Slide 52 text

Component.button( ButtonProperties, StyleSheet, Layout )

Slide 53

Slide 53 text

public struct ButtonProperties: AutoPropertyDiffable { public var text: String? public var isActive: Bool public var icon: Image? // sourcery: skipDiff public var onTap: MessageType? public init( text: String? = .none, isActive: Bool = false, icon: Image? = .none, onTap: MessageType? = .none) { self.text = text self.isActive = isActive self.icon = icon self.onTap = onTap } }

Slide 54

Slide 54 text

public struct ButtonProperties: AutoPropertyDiffable { public var text: String? public var isActive: Bool public var icon: Image? // sourcery: skipDiff public var onTap: MessageType? public init( text: String? = .none, isActive: Bool = false, icon: Image? = .none, onTap: MessageType? = .none) { self.text = text self.isActive = isActive self.icon = icon self.onTap = onTap } }

Slide 55

Slide 55 text

AutoPropertyDiffable.stencil

Slide 56

Slide 56 text

{% for type in types.implementing.AutoPropertyDiffable %} // MARK: - {{ type.name }} AutoPropertyDiffable {% if type.accessLevel == "public" %}public {% endif %}extension {{ type.name }} { {% if type.accessLevel == "public" %}public {% endif %}enum Property { {% for instanceVariable in type.instanceVariables|publicGet %} {% if not instanceVariable.annotations.ignoreInChangeSet %} {% if instanceVariable.type.based.AutoPropertyDiffable %} case {{ instanceVariable.name }}([{{ instanceVariable.type.name }}.Property]{% if instanceVariable.isOptional %}?{% endif %}) {% else %} case {{ instanceVariable.name }}({{ instanceVariable.typeName }}) {% endif %} {% endif %} {% endfor %} } {% if type.accessLevel == "public" %}public {% endif %}var fullChangeSet: [{{ type.name }}.Property] { return [ {% for instanceVariable in type.instanceVariables|publicGet %} {% if not instanceVariable.annotations.ignoreInChangeSet %} {% if instanceVariable.type.based.AutoPropertyDiffable %} .{{ instanceVariable.name }}(self.{{ instanceVariable.name }}{% if instanceVariable.isOptional %}?{% endif %}.fullChangeSet), {% else %} .{{ instanceVariable.name }}(self.{{ instanceVariable.name }}), {% endif %} {% endif %} {% endfor %} ] } {% if type.accessLevel == "public" %}public {% endif %}func changeSet(for {{ type.name|lowerFirst }}: {{ type.name }}) -> [{{ type.name }}.Property] { var changeSet: [{{ type.name }}.Property] = [] {% for instanceVariable in type.instanceVariables|publicGet %} {% if not instanceVariable.annotations.ignoreInChangeSet %} {% if instanceVariable.annotations.skipDiff %} {% if instanceVariable.type.based.AutoPropertyDiffable %} changeSet.append(.{{ instanceVariable.name }}({{ type.name|lowerFirst }}.{{ instanceVariable.name }}.fullChangeSet)) {% else %} changeSet.append(.{{ instanceVariable.name }}({{ type.name|lowerFirst }}.{{ instanceVariable.name }})) {% endif %} {% else %} {% if instanceVariable.type.based.AutoPropertyDiffable %} {% if instanceVariable.isOptional %} switch (self.{{ instanceVariable.name }}, {{ type.name|lowerFirst }}.{{ instanceVariable.name }}) { case (.some(let old), .some(let new)): let {{ instanceVariable.name }}ChangeSet = old.changeSet(for: new) if !{{ instanceVariable.name }}ChangeSet.isEmpty { changeSet.append(.{{ instanceVariable.name }}({{ instanceVariable.name }}ChangeSet)) } case (.none, .some(let new)): changeSet.append(.{{ instanceVariable.name }}(new.fullChangeSet)) case (.some(_), .none): changeSet.append(.{{ instanceVariable.name }}(.none)) case (.none, .none): break } {% else %} let {{ instanceVariable.name}}ChangeSet = self.{{ instanceVariable.name }}.changeSet(for: {{ type.name|lowerFirst }}.{{ instanceVariable.name }}) if !{{ instanceVariable.name}}ChangeSet.isEmpty { changeSet.append(.{{ instanceVariable.name }}({{ instanceVariable.name}}ChangeSet)) } {% endif %} {% else %} if self.{{ instanceVariable.name }} != {{ type.name|lowerFirst }}.{{ instanceVariable.name }} { changeSet.append(.{{ instanceVariable.name }}({{ type.name|lowerFirst }}.{{ instanceVariable.name }})) } {% endif %} {% endif%} {% endif%} {% endfor %} return changeSet } } {% endfor %}

Slide 57

Slide 57 text

public extension ButtonProperties { public enum Property { case text(String?) case isActive(Bool) case icon(Image?) case onTap(MessageType?) } public var fullChangeSet: [ButtonProperties.Property] { return [ .text(self.text), .isActive(self.isActive), .icon(self.icon), .onTap(self.onTap), ] } public func changeSet(for buttonProperties: ButtonProperties) -> [ButtonProperties.Property] { var changeSet: [ButtonProperties.Property] = [] if self.text != buttonProperties.text { changeSet.append(.text(buttonProperties.text)) } if self.isActive != buttonProperties.isActive { changeSet.append(.isActive(buttonProperties.isActive)) } if self.icon != buttonProperties.icon { changeSet.append(.icon(buttonProperties.icon)) } changeSet.append(.onTap(buttonProperties.onTap)) return changeSet } }

Slide 58

Slide 58 text

public extension ButtonProperties { public enum Property { case text(String?) case isActive(Bool) case icon(Image?) case onTap(MessageType?) } // ... }

Slide 59

Slide 59 text

public extension ButtonProperties { // ... public var fullChangeSet: [ButtonProperties.Property] { return [ .text(self.text), .isActive(self.isActive), .icon(self.icon), .onTap(self.onTap), ] } // ... }

Slide 60

Slide 60 text

public extension ButtonProperties { // ... public func changeSet(for buttonProperties: ButtonProperties) -> [ButtonProperties.Property] { var changeSet: [ButtonProperties.Property] = [] if self.text != buttonProperties.text { changeSet.append(.text(buttonProperties.text)) } if self.isActive != buttonProperties.isActive { changeSet.append(.isActive(buttonProperties.isActive)) } if self.icon != buttonProperties.icon { changeSet.append(.icon(buttonProperties.icon)) } changeSet.append(.onTap(buttonProperties.onTap)) return changeSet } }

Slide 61

Slide 61 text

public extension ButtonProperties { // ... public func changeSet(for buttonProperties: ButtonProperties) -> [ButtonProperties.Property] { var changeSet: [ButtonProperties.Property] = [] if self.text != buttonProperties.text { changeSet.append(.text(buttonProperties.text)) } if self.isActive != buttonProperties.isActive { changeSet.append(.isActive(buttonProperties.isActive)) } if self.icon != buttonProperties.icon { changeSet.append(.icon(buttonProperties.icon)) } changeSet.append(.onTap(buttonProperties.onTap)) return changeSet } }

Slide 62

Slide 62 text

public extension ButtonProperties { // ... public func changeSet(for buttonProperties: ButtonProperties) -> [ButtonProperties.Property] { var changeSet: [ButtonProperties.Property] = [] if self.text != buttonProperties.text { changeSet.append(.text(buttonProperties.text)) } if self.isActive != buttonProperties.isActive { changeSet.append(.isActive(buttonProperties.isActive)) } if self.icon != buttonProperties.icon { changeSet.append(.icon(buttonProperties.icon)) } changeSet.append(.onTap(buttonProperties.onTap)) return changeSet } }

Slide 63

Slide 63 text

fileprivate extension UIButton { fileprivate func apply(changeSet: [ButtonProperties.Property]) { for property in changeSet { switch property { case .text(let text): self.setTitle(text, for: .normal) case .icon(let maybeIcon): if let icon = maybeIcon { self.load(image: icon) { self.setImage($0, for: .normal) } } else { self.setImage(.none, for: .normal) } case .isActive(let isActive): self.isSelected = isActive case .onTap(let onTap): if let message = onTap { _ = self.on(event: .touchUpInside, dispatch: message) } else { let _: MessageDispatcher? = self.stopDispatchingMessages(for: .touchUpInside) } } } } }

Slide 64

Slide 64 text

case .text(let text): self.setTitle(text, for: .normal) case .icon(let maybeIcon): if let icon = maybeIcon { self.load(image: icon) { self.setImage($0, for: .normal) } } else { self.setImage(.none, for: .normal) }

Slide 65

Slide 65 text

AutoPropertyDiffable.generated.swift 1074 lineas

Slide 66

Slide 66 text

Soucery y metaprogramming permiten generar código para ahorrar tiempo, evitar problemas de mantenibilidad y dejarnos invertir tiempo en tareas que agregan más valor

Slide 67

Slide 67 text

¿PREGUNTAS?

Slide 68

Slide 68 text

Soucery y metaprogramming permiten generar código para ahorrar tiempo, evitar problemas de mantenibilidad y dejarnos invertir tiempo en tareas que agregan más valor