Types
Types are used in FatScript to combine data and behavior, acting as templates for creating new replicas.
Naming
Type names are case-sensitive and must start with an uppercase letter.
The recommended convention for type identifiers is PascalCase
.
Native Types
FatScript provides several native types:
- Any - anything
- Void - nothing
- Boolean - primitive
- Number - primitive
- HugeInt - primitive
- Text - primitive
- Method - function or lambda
- List - like array or stack
- Scope - like object or dictionary
- Error - yes, for errors
- Chunk - binary data
However, you need to import the types package to access the prototype members for each type.
Additional Types
FatScript's native types are augmented with a collection of extra types that build upon the core functionalities of its native types. Crafted in pure FatScript, these additional types cater to various advanced programming needs and facilitate common design patterns.
Moreover, you will find domain-specific types embedded within libraries, such as Worker
in the async library, FileInfo
in file, HttpRequest
(among others) in http, CommandResult
in system etc.
Custom Types
Besides using the types provided by the language or an external library, you may also create your own types, or extend existing ones with new behaviors.
Declaration
To define a custom type in FatScript, you can use a simple assignment statement. The type definition can be wrapped in either parentheses or curly brackets. Both syntaxes are valid and have the same effect. You may also optionally define default values for the type's properties, as shown in the following example:
# Type definition with default values
Car = (km: Number = 0, color: Text = 'white', optional = null)
Global Uniqueness
FatScript features a singular global meta-space, necessitating unique type names across your entire program and any included libraries. Attempting to define a type that shares a name with an existing type, even if in a different scope, triggers an AssignError
. However, if the new definition is identical, it will simply be ignored.
To survey the types present in the global meta-space, the command _<-fat.std; sdk.getTypes;
proves useful. This function enumerates all defined types, and details their definition locations with source:line:column
markers. This feature helps navigating and understanding the structure of your code and its dependencies.
It is wise to steer clear of names already in use by fat.std
library types when defining new types.
While FatScript does not impose a strict naming protocol for library development, adopting a conflict-averse naming strategy is recommended. A common practice involves prefixing type names with some unique identifier that reflects your library's name, thereby reducing the likelihood of name clashes.
Usage
To create instances of a custom type, call the type name as if it were a method, optionally passing values for the properties:
# Type usage from defaults
car = Car()
# outputs: { km: Number = 0, color: Text = 'white' }
# Type usage defaulting one of the properties
redCar = Car(color = 'red')
# outputs: { km: Number = 0, color: Text = 'red' }
# Type usage, fully qualified
oldCar1 = Car(color = 'blue', km = 38000)
# overrides both values
# Type usage, args using props sequence
oldCar2 = Car(41000, 'green')
# overrides values using type definition order
By default, custom types return a scope of their properties. If you define an apply
method, however, the type can return a different value. For example, here's a custom type Sum
with an apply
method that returns the sum of its a
and b
properties:
Sum = (a: Number, b: Number, apply = -> a + b)
Sum(1, 2) # output: 3
note that apply methods do have direct access to instance props
In this example, the output base type of apply
is a number, not a scope. This also means that the original properties of the custom type are lost during instantiation and cannot be accessed again.
Prototype members
Those are special kind of methods, stored inside the type definition:
TypeWithProtoMembers = {
~ a: Number
~ b: Number
setA = (newA: Number) -> self.a = newA
setB = (newB: Number) -> self.b = newB
sum = (): Number -> self.a + self.b
}
In this example, setA
, setB
and sum
are prototype members. Note that we needed to use self
, which is a keyword that provides a self reference to the instance (or method) scope, so that we could gain access to the props.
Checking types
If you're unsure about the type of an entry, you can simply check by comparing it with a type name:
place = 'restaurant'
place == Number # false
place == Text # true
alternatively, use the
typeOf
method from the SDK library to extract the type name
Anything can be compared with the reserved word Type
which identifies if it refers to a type:
Number == Type # true
Type
can also be used to specify that a method takes a type parameter:
combine = (t: Type, val: Any): Any -> ...
Type alias
In FatScript, you can create subtypes by aliasing an existing type. This means that the new type will inherit all of the properties of the base type. Here's an example:
_ <- fat.type.Text
Id = Text # creates an alias
Note that type aliases are hierarchical and can be used to classify values while still inheriting the same behavior. However, while the alias is considered equal to the base type, instances of the new type are not considered equal to the base type.
To check if a value is an instance of a type alias or its base type, you can use the less-equal comparison operator <=
. This allows you to accept any type on the alias chain, down to the base type. Here's an example:
Id == Text # true, as Id is an alias of Text
x = Id(123) # id: Id = '123'
x == Text # false, however x is Id it's not Text
x == Id # true, as expected x is of type Id
x <= Text # true, as x is of Id which is an alias of Text
This feature allows for fine-grained matching on specific types, while still maintaining the flexibility to use different aliases for the same underlying type.
limitation: it is not possible to create aliases for
Any
,Type
orMethod
Type constraints
In FatScript, you can declare type constraints for method parameters. When a method is called, the argument is automatically checked against the type constraint. If the argument is not of the expected type or one of its subtypes, a TypeError
is raised.
If the type constraint is a base type, any subtype of that type is also accepted as an argument. However, if the type constraint is a subtype, only arguments that match the subtype are accepted. Here's an example:
generalist = (x: Text) -> x
restrictive = (x: Id) -> x
In this example, the generalist
method accepts both Text
and Id
arguments, because Id
is a subtype of Text
. The restrictive
method only accepts Id
arguments and not Text
arguments, because Id
is a subtype of Text
, but not the other way around.
It's important to emphasize that custom types are derived from Scope
. In this context, Scope
would be the generalist type for, for instance, the custom type Car
.
Mixin (advanced)
When defining a type, you can add the features of an existing type simply by mentioning it on the type definition. This is called type inclusion or mixin.
For instance, to create a new type RentalCar
with the properties of Car
and an additional price
property, you can write:
RentalCar = {
# Includes
Car
# Additional prop
price: Number
}
RentalCar(50) # { color: Text = 'white', km: Number = 0, price: Number = 50 }
If a property is not defined in the new type, it will inherit the default value from the included type. In the above example, the color
and km
properties of Car
are present in RentalCar
, with their default values.
Inheriting prototype methods
Suppose we continue from the previous example of type TypeWithProtoMembers
that has two properties a
and b
, and three prototype methods setA
, setB
and sum
. To create a new type WithMoreMembers
that adds a property c
, a method setC
and overrides the sum
method, you can write:
WithMoreMembers = {
# Includes
TypeWithProtoMembers
# Props (instance parameters)
~ a: Number
~ b: Number
~ c: Number
# Prototype members (methods)
setC = (newC: Number) -> self.c = newC
sum = (): Number -> self.a + self.b + self.c
}
redeclaring the props allows the new type to also accept arguments at instantiation time, e.g.:
WithMoreMembers(1, 2, 3)
setsa
,b
andc
When creating a new instance of WithMoreMembers
, all four prototype methods setA
, setB
, setC
and sum
will be available.
Note that if there is a redefinition of a property or method in the new type, the new definition takes precedence.
Type casting
In FatScript, the *
symbol is used for type casting, allowing you to treat one data type as another without altering the underlying data. This capability is especially useful for explicitly specifying the type or for treating values as compatible types, for example:
time.format(Epoch * 1688257765448) # treats the number as a Unix Epoch value
Flexible type acceptance
FatScript offers flexibility in type acceptance through the inclusion of a base type. This system allows for the creation of interrelated types that can be interchangeably used in methods or as elements in a List.
For example, consider the types A
, B
, and C
. If types B
and C
exclusively incorporate type A
in their definitions, they are considered to share the same characteristics derived from A
, making B
and C
compatible types under the base of A
.
Here is how this looks in code:
A = (_)
B = (A, b = true)
C = (A, c = true)
# method1 accepts both types B and C
method1 = (a: A) -> 'valid'
# this logic also applies to lists
mixedList: List/A = [ B(), C() ]
type flexibility is only possible if the data type is based on
Scope
Caveat
This system allows a method designed to accept an object of type B
to also accept an object of type C
due to their common base in A
:
method2 = (x: B) -> 'valid'
method2(C()) # returns 'valid' (unexpectedly?)
Although the flexible system is generally useful, it may be inadequate when an exact type match is necessary. In such cases, the type could be explicitly verified within the method, for example, by using x == B
to accept only objects of type B
.
To restrict type flexibility and ensure an exact match, StrictType
should be included in the type definition:
C = (A, StrictType, c = true) # C now requires strict type matching
This modification prevents C
from being used where A
or B
are accepted, even though both share the same base type A
.
Composite types
In FatScript, composite types allow you to define complex data structures composed of simpler types to restrict parameter acceptance in methods and assignments. They are represented using slashes /
to separate the types within the composite type definition.
Let's go through a few examples and understand how composite types work:
ListOfNumbers = List/Number
, defines a composite typeListOfNumbers
, which is a list that can only contain numbers.Matrix = List/List/Number
, defines a composite typeMatrix
, which is a list of lists that can only contain numbers.MethodReturningListOfNumbers = Method/ListOfNumbers
, defines a composite typeMethodReturningListOfNumbers
, which is a method that returns aListOfNumbers
.NumericScope = Scope/Number
, defines a composite typeNumericScope
, which is a scope whose entries can only be of type number.