Tipos
Os tipos são usados no FatScript para combinar dados e comportamentos, atuando como modelos para a criação de novas réplicas.
Nomeação
Os nomes de tipo são sensíveis a maiúsculas e minúsculas, devendo começar com uma letra maiúscula.
A convenção recomendada para identificadores de tipo é PascalCase
.
Tipos Nativos
O FatScript fornece vários tipos nativos:
- Any - qualquer coisa
- Void - nada
- Boolean - primitivo
- Number - primitivo
- HugeInt - primitivo
- Text - primitivo
- Method - função ou lambda
- List - como uma matriz ou pilha
- Scope - como um objeto ou dicionário
- Error - sim, para erros
- Chunk - dados binários
No entanto, é necessário importar o pacote type para acessar os membros de protótipo de cada tipo.
Tipos Adicionais
Os tipos nativos do FatScript são enriquecidos com uma coleção de tipos extras que expandem as funcionalidades básicas de seus tipos nativos. Criados em FatScript puro, esses tipos adicionais atendem a várias necessidades de programação avançada e facilitam padrões de projeto comuns.
Além disso, você encontrará tipos de domínio específico incorporados nas bibliotecas, como Worker
na biblioteca async, FileInfo
em file, HttpRequest
(entre outros) em http, CommandResult
em system etc.
Tipos Personalizados
Além de usar os tipos fornecidos pela linguagem ou por uma biblioteca externa, você também pode criar seus próprios tipos ou estender os existentes com novos comportamentos.
Declaração
Para definir um tipo personalizado no FatScript, você pode usar uma simples declaração de atribuição. A definição de tipo pode ser envolvida em parênteses ou chaves. Ambas as sintaxes são válidas e têm o mesmo efeito. Você também pode opcionalmente definir valores padrão para as propriedades do tipo, como mostrado no seguinte exemplo:
# Definição de tipo com valors padrão
Carro = (km: Number = 0, cor: Text = 'branco', opcional = null)
Unicidade Global
O FatScript possui um único meta-espaço global, exigindo que os nomes dos tipos sejam únicos em todo o seu programa e em quaisquer bibliotecas incluídas. Tentar definir um tipo que compartilha um nome com um tipo existente, mesmo que em um escopo diferente, aciona um AssignError
. No entanto, se a nova definição for idêntica à existente, ela será simplesmente ignorada.
Para examinar os tipos presentes no meta-espaço global, o comando _<-fat.std; sdk.getTypes;
se mostra útil. Esta função enumera todos os tipos definidos e detalha seus locais de definição com marcadores source:line:column
. Este recurso ajuda na navegação e compreensão da estrutura do seu código e suas dependências.
É prudente evitar nomes já utilizados pelos tipos de biblioteca fat.std
ao definir novos tipos.
Embora o FatScript não imponha um protocolo estrito de nomes para o desenvolvimento de bibliotecas, é recomendado adotar uma estratégia de nomes que evite conflitos. Uma prática comum envolve prefixar os nomes dos tipos com algum identificador único que reflita o nome da sua biblioteca, reduzindo assim a probabilidade de choques de nomes.
Uso
Para criar instâncias de um tipo personalizado, chame o nome do tipo como se fosse um método, opcionalmente passando valores para as propriedades:
# Uso do tipo com padrões
carro = Carro()
# saída: { km: Number = 0, cor: Text = 'branco' }
# Uso do tipo, definindo uma das propriedades
carroVermelho = Carro(cor = 'vermelho')
# saída: { km: Number = 0, cor: Text = 'vermelho' }
# Uso do tipo, totalmente qualificado
carroVelho1 = Carro(cor = 'azul', km = 38000)
# substitui ambos os valores
# Uso do tipo, argumentos usando a sequência das propriedades
carroVelho2 = Carro(41000, 'verde')
# substitui valores usando a ordem da definição do tipo
Por padrão, os tipos personalizados retornam um escopo de suas propriedades. No entanto, se você definir um método apply
, o tipo poderá retornar um valor diferente. Por exemplo, aqui está um tipo personalizado Soma
com um método apply
que retorna a soma de suas propriedades a
e b
:
Soma = (a: Number, b: Number, apply = -> a + b)
Soma(1, 2) # saída: 3
observe que os métodos apply têm acesso direto às propriedades da instância
Neste exemplo, o tipo base de saída do apply
é um número, não um escopo. Isso também significa que as propriedades originais do tipo personalizado são perdidas durante a instanciação e não podem ser acessadas novamente.
Membros do protótipo
Esses são um tipo especial de método, armazenados dentro da definição do tipo:
TipoComMembrosDePrototipo = {
~ a: Number
~ b: Number
setA = (novoA: Number) -> self.a = novoA
setB = (novoB: Number) -> self.b = novoB
soma = (): Number -> self.a + self.b
}
Neste exemplo, setA
, setB
e soma
são membros do protótipo. Observe que precisamos usar self
, que é uma palavra-chave que fornece uma referência ao escopo da própria instância (ou método), para que nós pudéssemos ganhar acesso às propriedades.
Checando tipos
Se você não sabe qual é o tipo de uma entrada, pode simplesmente verificar comparando com um nome de tipo:
lugar = 'restaurante'
lugar == Number # false
lugar == Text # true
alternativamente, use o método
typeOf
da biblioteca SDK para extrair o nome do tipo
Qualquer coisa pode ser comparada com a palavra reservada Type
, que identifica se se refere a um tipo:
Number == Type # true
Type
também pode ser usado para especificar que um método recebe um parâmetro de tipo:
combine = (t: Type, val: Any): Any -> ...
Alias de tipo
No FatScript, você pode criar subtipos atribuindo um nome diferente a um tipo existente. Isso significa que o novo tipo herdará todas as propriedades do tipo base. Aqui está um exemplo:
_ <- fat.type.Text
Id = Text # cria um alias
Observe que os aliases de tipo são hierárquicos e podem ser usados para classificar valores enquanto ainda herdam o mesmo comportamento. No entanto, embora o alias seja considerado igual ao tipo base, as instâncias do novo tipo não são consideradas iguais ao tipo base.
Para verificar se um valor é uma instância de um alias de tipo ou do tipo base, você pode usar o operador de comparação de menor-ou-igual <=
. Isso permite que você aceite qualquer tipo na cadeia de aliases, até o tipo base. Aqui está um exemplo:
Id == Text # verdadeiro, já que Id é um alias de Text
x = Id(123) # id: Id = '123'
x == Text # falso, no entanto x é do tipo Id, não Text
x == Id # verdadeiro, como o esperado x é do tipo Id
x <= Text # verdadeiro, já que x é do tipo Id, que é um alias de Text
Essa funcionalidade permite uma validação refinada em tipos específicos, mantendo a flexibilidade de usar diferentes aliases para o mesmo tipo subjacente.
limitação: não é possível criar alias para
Any
,Type
ouMethod
Restrições de tipo
No FatScript, você pode declarar restrições de tipo para parâmetros de método. Quando um método é chamado, o argumento é verificado automaticamente em relação à restrição de tipo. Se o argumento não for do tipo esperado ou um de seus subtipos, um TypeError
é gerado.
Se a restrição de tipo for um tipo base, qualquer subtipo desse tipo também será aceito como argumento. No entanto, se a restrição de tipo for um subtipo, somente argumentos que correspondam ao subtipo serão aceitos. Aqui está um exemplo:
generalista = (x: Text) -> x
restritivo = (x: Id) -> x
Neste exemplo, o método generalista
aceita argumentos Text
e Id
, porque Id
é um subtipo de Text
. O método restritivo
aceita apenas argumentos Id
e não Text
, porque Id
é um subtipo de Text
, mas não o contrário.
É importante enfatizar que os tipos personalizados são derivados de Scope
. Nesse contexto, Scope
seria o tipo generalista para, por exemplo, o tipo personalizado Carro
.
Mixin (avançado)
Ao definir um tipo, você pode adicionar os recursos de um tipo existente simplesmente mencionando-o na definição de tipo. Isso é chamado de inclusão de tipo ou mixin.
Por exemplo, para criar um novo tipo CarroAlugado
com as propriedades de Carro
e uma propriedade adicional preco
, você pode escrever:
CarroAlugado = {
# Inclusões
Carro
# Propriedade adicional
preco: Number
}
CarroAlugado(50) # { cor: Text = 'branco', km: Number = 0, preco: Number = 50 }
Se uma propriedade não estiver definida no novo tipo, ela herdará o valor padrão do tipo incluído. No exemplo acima, as propriedades cor
e km
do Carro
estão presentes no CarroAlugado
, com seus valores padrão.
Herdando métodos de protótipo
Suponha que continuemos a partir do exemplo anterior do tipo TipoComMembrosDePrototipo
que tem duas propriedades a
e b
, e três métodos de protótipo setA
, setB
e soma
. Para criar um novo tipo ComMaisMembros
que adiciona uma propriedade c
, um método setC
e substitui o método soma
, você pode escrever:
ComMaisMembros = {
# Inclusões
TipoComMembrosDePrototipo
# Propriedades (parâmetros da instância)
~ a: Number
~ b: Number
~ c: Number
# Membros de protótipo (métodos)
setC = (novoC: Number) -> self.c = novoC
soma = (): Number -> self.a + self.b + self.c
}
redeclarando as propriedades permite que o novo tipo também aceite argumentos no momento da instanciação, por exemplo:
ComMaisMembros(1, 2, 3)
definea
,b
ec
Ao criar uma nova instância de ComMaisMembros
, todos os quatro métodos de protótipo setA
, setB
, setC
e soma
estarão disponíveis.
Observe que se houver uma redefinição de uma propriedade ou método no novo tipo, a nova definição terá precedência.
Conversão de tipos
No FatScript, o símbolo *
é usado para conversão de tipo, permitindo que você trate um tipo de dado como outro sem alterar os dados subjacentes. Essa capacidade é especialmente útil para especificar explicitamente o tipo ou para tratar valores como tipos compatíveis, por exemplo:
time.format(Epoch * 1688257765448) # trata o número como um valor de Época Unix
Aceitação flexível de tipos
FatScript oferece flexibilidade na aceitação de tipos por meio da inclusão de um tipo base. Este sistema permite a criação de tipos inter-relacionados que podem ser utilizados de forma intercambiável em métodos ou como elementos de uma Lista.
Por exemplo, consideremos os tipos A
, B
, e C
. Se os tipos B
e C
incorporam o tipo A
de maneira exclusiva em suas definições, eles são considerados como compartilhando as mesmas características derivadas de A
, tornando B
e C
tipos compatíveis sob a base de A.
Aqui está como isso parece no código:
A = (_)
B = (A, b = true)
C = (A, c = true)
# method1 aceita ambos tipos B e C
method1 = (a: A) -> 'valid'
# essa lógica também se aplica a listas
mixedList: List/A = [ B(), C() ]
a flexibilidade do tipo só é possível se o tipo de dados é baseado em
Scope
Advertência
Este sistema permite que um método projetado para aceitar um objeto do tipo B
também possa aceitar um objeto do tipo C
devido à base comum em A
:
method2 = (x: B) -> 'valid'
method2(C()) # retorna 'valid' (inesperadamente?)
Embora o sistema flexível seja geralmente útil, ele pode ser inadequado quando é necessária uma correspondência exata de tipos. Nesses casos, poderia-se verificar explicitamente o tipo dentro do método, por exemplo, usando x == B
para aceitar apenas objetos do tipo B
.
Para restringir a flexibilidade de tipo e garantir correspondência exata, deve-se incluir StrictType
na definição do tipo:
C = (A, StrictType, c = true) # C agora exige correspondência estrita de tipo
Esta modificação impede que C
seja utilizado onde A
ou B
são aceitos, mesmo que ambos compartilhem do mesmo tipo base A
.
Tipos compostos
No FatScript, tipos compostos permitem que você defina estruturas de dados complexas compostas por tipos mais simples para restringir aceitação de parâmetros em métodos e atribuições. Eles são representados usando barras /
para separar os tipos na definição do tipo composto.
Vamos ver alguns exemplos e entender como os tipos compostos funcionam:
ListOfNumbers = List/Number
, define um tipo compostoListOfNumbers
, que é uma lista que só pode conter números.Matrix = List/List/Number
, define um tipo compostoMatrix
, que é uma lista de listas que só pode conter números.MethodReturningListOfNumbers = Method/ListOfNumbers
, define um tipo compostoMethodReturningListOfNumbers
, que é um método que retorna umListOfNumbers
.NumericScope = Scope/Number
define um tipo compostoNumericScope
, que é um escopo cujas entradas podem ser apenas do tipo número.