O ano de 2014 trouxe inovação tecnológica sem precedentes para a Apple: nasceu Swift, uma linguagem de programação trazendo sintaxe moderna e concisa, um type system mais poderoso, suporte first class a técnicas de programação funcional e interoperabilidade total com bases de código legadas escritas em Objective-C. De 2014 a 2018, Swift passou de uma promessa, para uma das linguagens de programação mais populares do mundo, graças em particular ao seu processo de evolução totalmente open source.

Na Sympla, usamos Swift para escrever nossos apps iOS totalmente do zero e hoje somos um dos principais da App Store brasileira. A experiência da Sympla com Swift passa extensamente por todas as features da linguagem, mas neste post vamos focar no type system da linguagem e nas técnicas, erros e acertos que vivenciamos ao explorá-lo em profundidade.

O type system de Swift é sofisticado, robusto e moderno. Ele permite que o programador expresse grande parte das intenções do código usando as definições de tipos, muito mais do que era possível em Objective-C. Em Swift é possível codificar regras de negócio e relações entre as camadas de aplicação no type system, resultando em um conjunto muito maior de bugs que podem ser prevenidos em tempo de compilação.

Optionals

Uma das ferramentas fundamentais oferecidas pela linguagem para aumentar a superfície de prevenção de bugs em tempo de compilação da sua base de código é o conceito de optionals.

O tipo Optional define uma estrutura de dados que representa a possibilidade de ausência de valor.

enum Optional<T> {
  case none
  case some(T)
}

var optionalInt: Int? // Optional<Int>

Variáveis do tipo Int? podem receber valores do tipo Int, mas também acomodam um valor especial nil que representa a ausência de valor.

optionalInt = nil // .none
optionalInt = 1 // .some(1)

Para o compilador, os tipos Int? e Int estão de certa forma relacionados, mas na prática são distintos. Não é possível combinar diretamente valores do tipo Int com valores do tipo Int? usando os operadores built-in da linguagem, por exemplo, pois eles trabalham apenas com valores de tipos primitivos e iguais entre si.

let nonOptionalInt: Int = 5

// 7
print(nonOptionalInt + 2)

// Value of optional type 'Int?' not unwrapped; did you mean to use '!' or '?'?
print(nonOptionalInt + optionalInt)

Um valor do tipo Int? pode ser interpretado com um valor do tipo Int com um contexto semântico adicional: a possibilidade de que este valor esteja ausente. Para somar um valor do tipo Int? a um valor do tipo Int, o programador precisa “extrair” o valor do tipo Int que está embrulhado dentro deste contexto.

// Extração segura
if let safelyUnwrappedInt = optionalInt {
 print(safelyUnwrappedInt + nonOptionalInt)
}

// Extração insegura e perigosa
let explictlyUnwrappedInt = optionalInt!
print(explictlyUnwrappedInt + nonOptionalInt)

O tipo Event da camada de modelo do app iOS da Sympla possui um campo image do tipo URL?, pois eventos podem ser cadastrados na plataforma sem imagem associada.

Na primeira versão do app escrita em Objective-C este tipo foi definido sem verificações do compilador para a possibilidade de ausência de valor no campo image.

@interface Event: NSObject

@property (nonatomic, strong, readonly) NSString *name;
@property (nonatomic, strong, readonly) NSURL * image;

@end

// ...

@implementation EventTableViewCell

- (void) render:(Event *)event {
 // Possível bug
 [self.imageView setImageWithURL:event.image];
}

@end

A primeira versão deste código não verificava se o campo event.image continha algum valor antes de ser usado. Neste caso, o código não contemplou o requisito de negócio de mostrar um placeholder para eventos sem imagem.

struct Event {
   let title: String
   let image: URL?
 }
 
 // ...
 
 class EventTableViewCell: UITableViewCell {
   func render(event: Event) {
     // Erro de compilação!
     imageView.setImage(url: event.image)
   }
 }

Álgebra no type system

Outra técnica muito utilizada para aumentar a segurança da base de código iOS da Sympla, é usar noções de álgebra para escrever definições de tipo. A álgebra pode se manifestar no type system de muitas maneiras, mas na Sympla focamos em usar conceitos algébricos para controlar o número de instâncias possíveis de cada tipo.

Product types

Sob esta ótica, tipos que são definidos como structs em Swift são classificados como product types: o número de instâncias possíveis de uma struct é o produto do número de instâncias possíveis dos tipos de cada um dos campos da struct.

/*
   |DateRange| = |DateRangeIdentifier| * |Date| * |Calendar|
               = 6 * |Date| * |Calendar|
 */
 struct DateRange {
   private let identifier: DateRangeIdentifier
   private let base: Date
   private let calendar: Calendar
 }
 
 // |DateRangeIdentifier| = 6
 enum DateRangeIdentifier {
   case today
   case tomorrow
   case thisWeek
   case thisWeekend
   case nextWeek
   case thisMonth
 }

Sum types

Tipos definidos como enums são classificados como sum types: o número de instâncias possíveis de uma enum é a soma do número de instâncias possíveis dos tipos envolvidos na definição da enum.

/*
   Enums são sum types
 
   |FormFieldStatus| = 1 + |String?| + 1
                     = 1 + (1 + |String|) + 1
                     = 3 + |String|
 */
 enum FormFieldStatus {
   case idle
   case invalid(message: String?)
   case valid
 }
 
 // Lembrando que:
 typealias String? = Optional

Tornando impossível representar situações impossíveis

Um caso de uso notável de enums na base de código iOS da Sympla é a modelagem dos estados possíveis de uma tela.

O tipo SearchResultsState modela os estados possíveis da lista de eventos que aparecem como resultado de uma busca no app iOS do comprador.

// |SearchResultsState| = |[Event]| * |Bool| * |Error?|
                         = |[Event]| * 2 * (1 + |Error|)
 struct SearchResultsState {
   let data: [Event]
   let isLoading: Bool
   let error: Error?
 }

O problema dessa definição é acomodar muito mais estados do que fazem sentido sob a ótica das regras de negócio da busca de eventos. Como consequência, o programador pode enfrentar dúvidas ao materializar a UI que deriva do estado atual da lista de eventos.

func render(state: SearchResultsState) {
   // ?
 }

O uso uma struct para definir os estados possíveis da lista de eventos agravado pela falta de documentação dá margem para que o programador implemente a UI de forma errônea ou esqueça de concretizar requisitos:

  • Sempre que o array de eventos for vazio, deve-se mostrar uma mensagem?
  • Se o array não está vazio mas existem dados sendo carregados, o indicador de atividade tem precedência sobre a mensagem?
  • A mensagem de erro deve ser exibida sempre que o carregamento falha ou somente se o array de eventos está vazio?

Não é possível responder a estas perguntas somente com base na definição do tipo SearchResultsState. Porém, o refactoring que transforma esta struct em uma enum é muito simples e permite que as perguntas sejam respondidas imediatamente com base na leitura do código. Mais: o compilador passa a ser um aliado e pode garantir em tempo de compilação que é impossível representar um estado inválido.

// |SearchResultsState| = 1 + |[Event]| + |Error|
 //                      = 1 + |[]| + |[x:xs]| + |Error|
 enum SearchResultsState {
   case loading
   case loaded(data: [Event])
   case failed(error: Error)
 }

Esta definição é muito mais estrita e acomoda apenas quatro estados possíveis:

  • Carregando dados
  • Dados carregados
  • Lista vazia
  • Falha no carregamento

Como consequência, o formato em alto nível do método render se torna muito mais claro, sem espaço para outras interpretações.

func render(state: SearchResultsState) {
   switch state {
   case .loading:
     renderLoading()
   case .loaded(let data) where data.isEmpty:
     renderEmpty()
   case .loaded(let data):
     renderLoaded(events: data)
   case .failed(let error):
     renderFailed(error: error)
   }
 }

Phantom types

Um phantom type é um tipo parametrizado que não utiliza os parâmetros de tipo em sua definição. Eles são muito úteis, por exemplo, para rotular variáveis estaticamente em tempo de compilação usando as assinaturas de tipo.

Na base de código iOS da Sympla usamos o conceito de phantom types para definir os tipos dos identificadores das entidades da camada de modelo. Os identificadores de cada instância de uma entidade são Strings que identificam unicamente cada instância no banco de dados da Sympla. Tomemos como exemplo o tipo Order que identifica pedidos de ingressos. A definição original deste tipo era assim:

struct Order {
   typealias Id = String
 
   let identifier: Id
   let purchaseDate: Date
   let tickets: [Ticket]
 
   // ...
 }

Um possível problema que decorre desta definição é que apesar de todos os identificadores das entidades da Sympla serem modelados usando Strings, o identificador de uma instância do tipo Ticket não faz sentido para uma instância do tipo Order (e vice-versa). Para resolver esta potencial fonte de bugs de lógica, definimos o tipo Tagged que representa um valor embrulhado.

struct Tagged<Tag, Value> {
   let value: Value
 
   init (_ value: Value) {
     self.value = value
   }
 }

O tipo Tagged é especial pois além de ser parametrizado com o tipo do valor embrulhado ele também é parametrizado com um tipo que chamaremos de Tag. O parâmetro de tipo Tag não é usado na definição do tipo Tagged, mas funciona como um rótulo estático que permite, por exemplo, que Tagged<Order, String> seja um tipo distinto de Tagged<Ticket, String> sob os olhos do compilador.

struct Order {
   typealias Id = Tagged<Order, String>
 
   let identifier: Id
   let purchaseDate: Date
   let tickets: [Ticket]
 
   // ...
 }
 
 struct Ticket {
   typealias Id = Tagged<Ticket, String>
 
   let identifier: Id
   let participant: Participant
   let price: NSDecimalNumber
 
   // ...
 }

Esta técnica impede que um identificador de uma instância de Ticket seja usado quando o identificador de uma instância de Order é esperado, por exemplo.

struct GetEditableParticipants: HTTPResource {
   private let orderIdentifier: Order.Id
 
   init (orderIdentifier: Order.Id) {
     self.orderIdentifier = orderIdentifier
   }
 
   var method: HTTPMethod {
     return .GET
   }
 
   var parameters: [String: Any] {
     return [
       "order": orderIdentifier.rawValue
     ]
   }
 
   // ...
 }

Se tentarmos criar uma instância de GetEditableParticipants usando o identificador de uma instância de Ticket (ou de qualquer outra entidade do modelo) seremos apresentados a um erro de compilação. Usar o identificador de uma instância de Order funciona normalmente.

var editableParticipantsRequest: GetEditableParticipants
 let order: Order = /* ... */
 let ticket: Ticket = /* ... */
 
 /* error: cannot convert value of type 'Ticket.Id' (aka 'Tagged<Ticket, String>')
    to expected argument type 'Order.Id' (aka 'Tagged<Order, String>') */
 editableParticipants = GetEditableParticipants(
   orderIdentifier: ticket.identifier
 )
 
 // OK
 editableParticipants = GetEditableParticipants(
   orderIdentifier: order.identifier
 )

Conclusão

Swift é uma linguagem muito versátil. Para iniciantes ela oferece uma sintaxe ergonômica, enxuta e familiar, permitindo que a linguagem seja abordada aos poucos de forma prazerosa. Para os profissionais da indústria ela fornece todas as ferramentas para resolver os problemas do cotidiano da melhor forma possível. Mas Swift é muito mais do que isso. Por trás da ergonomia e da facilidade de adoção existe uma linguagem poderosa, sofisticada e moderna com muito a oferecer.

Existem muitas outras formas de explorar o type system da linguagem e fazer o compilador trabalhar a seu favor. Codificar regras de negócio na definição dos tipos abre espaço para modelos de programação que não eram possíveis em Objective-C, e pode ajudar a prevenir em tempo de compilação o surgimento de um conjunto grande de bugs. A programação funcional moderna encontrada em linguagens como Haskell por exemplo faz uso extenso de técnicas como as apresentadas aqui e exercita o type system de formas muito criativas. Tanto o type system quanto as features de Swift possibilitam que estas ideias sejam exploradas para melhorar bases de código que existem hoje.

 

Desenvolvedor iOS sênior com 7+ anos de experiência implementando aplicações iOS de nível enterprise para segmentos de negócio variados como mídia esportiva, telecomunicações, entrega de comida, educação e gerenciamento de eventos. Também tenho experiência desenvolvendo aplicações React Native e web full stack.

Posted by:Fellipe Caetano

Desenvolvedor iOS sênior com 7+ anos de experiência implementando aplicações iOS de nível enterprise para segmentos de negócio variados como mídia esportiva, telecomunicações, entrega de comida, educação e gerenciamento de eventos. Também tenho experiência desenvolvendo aplicações React Native e web full stack.

Deixe seu comentário