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:

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 or Method

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

  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 ""