Types

Types are used in FatScript to combine data and behavior, acting as templates for creating new replicas (instances).

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:

However, you need to import the types package to access the prototype members for each type.

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 examples:

# Type definition using curly brackets
Car = { km: Number, color: Text }

# Type definition using parentheses with default values
Car = (km = 0, color = 'white')

Global Uniqueness

Although the type definition is stored in the scope where it is declared, the type name must be unique within your program. If you try to define a type with the same name as an existing one, even in a different scope, an AssignError will be raised, unless the definition is identical, in which case it will be ignored.

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 an embedded command that provides a self reference to the instance (or method) scope, so that we could gain access to the props.

Checking types

If you don't know what is the type of an entry you can simply check by comparing with a typename:

place = 'restaurant'
place == Number  # false
place == Text    # true

alternatively, use typeOf method from sdk lib to extract the typename

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, Void, List, or Method

Type constraints

In FatScript, you can declare type constraints for method arguments. 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.

Type inclusions (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.

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 arguments)
  ~ 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) sets a, b and c

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 serves as a type cast operator, allowing you to convert one data type into another. This feature is particularly useful when you need to explicitly specify the type or perform conversions between compatible types, e.g.:

time.format(Epoch * 1688257765448)  # coerces the number into Unix Epoch

Flexible type acceptance

FatScript offers flexibility of type acceptance by implementing a system based on type inclusion. This creates interrelated types that can be interchangeably used within a method or as List items.

When you define a type, it's possible to incorporate one or more additional types within that definition. Take, for example, types A, B, and C. If types B and C both include type A in their definitions, then they are seen as sharing the same set of characteristics derived from A. This means B and C are viewed as sibling types under the umbrella of A.

This system enables a method that is designed to accept an object of type B to also be capable of accepting an object of type C, and vice versa. This is due to the fact that both types B and C share a common basis in type A.

Here's how it looks in code:

A = (_)
B = (A, b = true)
C = (A, c = true)

# method1 accepts both B and C because they both include A
method1 = (a: A) -> ...

# method2 accepts C since both B and C include the same set of types
# (making them sibling types)
method2 = (x: B) -> ...

# this logic also applies to List types, as seen with mixedList
mixedList: List/A = [ B(), C() ]

type flexibility is only possible if the data type is based on Scope

Caveat

You may have to explicitly check the type, e.g. x == B inside the method body if you only want to handle B, but not C on your method. Or you can create an alias, e.g. D = A and use C = (D, c = true) as type inclusion to avoid flexible behavior altogether.

Composite types

In FatScript, composite types allow you to define complex data structures composed of simpler types. 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:

  1. ListOfNumbers = List/Number, defines a composite type ListOfNumbers, which is a list that can only contain numbers.

  2. Matrix = List/List/Number, defines a composite type Matrix, which is a list of lists that can only contain numbers.

  3. MethodReturningListOfNumbers = Method/ListOfNumbers, defines a composite type MethodReturningListOfNumbers, which is a method that returns a ListOfNumbers.

  4. NumericScope = Scope/Number, defines a composite type NumericScope, which is a scope whose entries can only be of type number.

See also

results matching ""

    No results matching ""