We are currently working on new rules for what content should and shouldn't be allowed on this website, and are looking for feedback! See Esolang:2026 topicality proposal to view and give feedback on the current draft.
PhantomPL
PhantomPL is designed by PSTF.
Overview
PhantomPL, as a practical programming language, is mostly used for data science and backend development.
Because it is used for data science, it is mostly inspired from Python.
Volume 1: Basic Syntax
Chapter 0: Our First Program
#> Hello.ptpl
print("Hello, World!")
Chapter 1: Program Structure
PhantomPL programs don't need an explicit entry point like C or Rust. Unless a main function is defined, the program usually starts at the first line. The interpreter should scan over the program and check if there is a main function. If so, run the program from the main function, otherwise run from the first line.
Chapter 2: Data Types and Structures
Section 1: Data Types
PhantomPL supports the following types of data:
- Integer. A sequence of Indo-Arabic numerals is called an integer. PhantomPL has no limits on integer precision, so you can create integers of any size. Some examples of integers are 3, 15, 114514, -28, and so on.
- Decimals. A decimal is just a sequence of two sets of numbers separated by a decimal point. Similarly, PhantomPL doesn't have precision limits for decimals, so you won't run into situations like 0.1 plus 0.2 equaling 0.30000000000000004. To distinguish between finite and infinite decimals, PhantomPL lets you add three dots after a decimal to indicate it's infinite (accurate up to the part before the three dots). If there are at least three repeating cycles before the three dots, PhantomPL will treat it as a rational number.
- String. A string is a sequence of characters enclosed in double or single quotes. You can think of it as a list, and it's an indexable data structure. It supports some escape sequences. Other types of strings include f-strings (formatted strings) and r-strings (raw strings or regular expressions), as well as multi-line strings (enclosed in triple or six quotes).
- Boolean value. A value that is either true or false. Everything empty can be considered false, and all other values can be considered true. 0 is actually a kind of empty value. Empty strings, empty lists, and so on are all empty values.
- List. PhantomPL's lists are similar to Python's lists and Polynomix's arrays, and they can store any type, any value, and any number of data.
- Pair. A pair of value with same or different types. Pairs can also contain pairs.
- Void. It is a common subclass of all types. It has only one value, void, which is also its type symbol. When it is converted to a value of another type, it is converted to the default value of the target type, for example, 0.
- Key-value pairs. Similar to ordered pairs, key-value pairs also consist of two elements, but the first element must be a string, and the separator is a colon instead of a comma. For convenience, we call a list where each element is a key-value pair a dictionary.
- Automatic type (Everything). All types are its subclasses. Depending on the value, it can automatically guess and convert to the target data type.
- Function. A function reference, sometimes also called a subroutine or lambda.
- Pointer. A pointer is an address reference, and its size depends on your memory size.
- Type. A type itself is also a data type. It can't be converted to or from anything.
Section 2: Variable Declaration
let [modifier] variable_name [(variable_type)] <- variable_value;
This statement declares a variable. A variable's identifier has to follow these rules: it must start with an XID_start and be followed by zero, one, or more XID_continue characters. Under Unicode normalization form C (NFC), identifiers that look the same are treated as identical, for example, the Kelvin sign and the Latin capital letter K are considered the same character. Identifiers that match keywords aren't valid normal identifiers, but they are valid 'raw identifiers.' Raw identifiers start with $ and end with |$. PhantomPL uses Unicode standard version 17.0.0.
If you don't understand XID_start and XID_continue, here's a more beginner-friendly way to put it:
- It has to be made up of characters that aren't special symbols, including uppercase letters, lowercase letters, standard numbers, underscores, and some characters from other languages.
- The first character can't be a number.
- Don't use a name that's the same as a keyword.
- Try to make two identifiers look different, or else others—or even the compiler or interpreter—might think they're the same identifier. For example, try to avoid using symbols that look similar, like 0x212A (K) and 0x004B (K).
- Although it's not a strict requirement, try to keep identifiers simple and clear. If possible, avoid overly simple single-letter identifiers and overly complicated or ritually overly proper long identifiers. Programming isn't feudal.
Besides, you can not assign to "_" because it is reserved as a wildcard variable.
Section 3: Modifier
The available modifiers include mutability modifiers, visibility modifiers, and static/dynamic modifiers.
Mutability modifiers include var (mutable) and const (immutable), meaning variable and constant, respectively. By default, variables are mutable (var).
Visibility modifiers include public, global, local, and private, which will be explained in detail in the "Variable Visibility" chapter. By default, a variable's visibility is global, which you can simply think of as "visible to the current program" for now.
Static/dynamic modifiers mainly include static (other static/dynamic modifiers aren't used in this tutorial, but they are also part of PhantomPL). These modifiers affect how class members are stored and referenced.
Chapter 3: Expressions and Codes
Section1: Expressions
Any expression that can be evaluated is a valid PhantomPL expression. Expressions are divided into value expressions and code.
let voltage <- 5.0; let bit (bool) <- (voltage > 2.5)?true:false;
In these two expressions, we define the voltage of a certain bit as 5 volts, and then we define the bit's state based on the rule that if the voltage is above 2.5 volts, it's 1, otherwise it's 0.
print("Hello, World!");
This expression doesn't return anything, but it will print "Hello, World!" on the screen.
let number <- 1;
for _ in interval(1, 100, 1, true, true):
number <- number * _;
rof
In the end, the number should equal 100!, which is 93 326 215 443 944 152 681 699 238 856 266 700 490 715 968 264 381 621 468 592 963 895 217 599 993 229 915 608 941 463 976 156 518 286 253 697 920 827 223 758 251 185 210 916 864 000 000 000 000 000 000 000 000(grouped by spaces), a huge 158-digit number.
| Operator | Name | Description |
|---|---|---|
| - | Negative | For integers or decimals, take their opposite. For lists or strings, reverse them. For pairs of numbers, swap the first and second values. For key-value binds, negate the value. |
| not | Logical NOT | Evaluates into false if the original expression was true. |
| ! | Named parameter indicator | Indicates that the parameter is named. |
| + | Addition | If both operands are numbers, add them together. If both operands are lists or strings, append the second to the first. |
| - | Subtraction | Like what in mathematics. |
| * | Multiplication | If both operands are numbers, return their product. If one is a string or list and the other is a natural number n, repeat it n times and return it. |
| / | Division | Like what in Python. |
| // | Truncated Division | Like what in Python. |
| % | Modulo | Like what in Python. |
| ^ | Exponent | Like what in Xonovile. |
| ^^ | Double exponent | Not really common, but anyway, x^^y returns where there are y layers of x.
|
| & | Bitwise AND | Literally meaning. |
| | | Bitwise OR | Literally meaning. |
| or | Logical OR | Returns true if one of the expression is true. |
| and | Logical AND | Returns true onlt if both expression is true. |
| ~ | Bitwise NOT | Literally meaning. |
| @ | Bitwise XOR | Literally meaning. |
| # | Neglection | Inline comment. |
| << | Left Shift | Multiply x by 2y. |
| >> | Right Shift | Divide x by 2y. If it's used on integers, round the result toward 0. |
| <- | Assignment | Assign y to x. |
| ?$ XXXXX $? | Document Comment | Literally meaning. |
| [}$ XXXXX ${] | Markdown-style Comment | Literally meaning. |
| do XXXXX end | Code block delimiter | Literally meaning. |
| = | Equality | Returns true if x is equal to y. |
| != | Unequality | Returns false if x is equal to y. |
| > | Superior | Returns true if x is greater than y. |
| <= | Non-Superior | Returns false if x is greater than y. |
| < | Inferior | Returns true if x is less than to y. |
| >= | Non-inferior | Returns false if x is less than y. |
| b?a:c | Conditional | Evaluates into a if b is true else c. |
Section 2: Control Flows
Conditional-branch
The format of a basic conditional statement is shown as follows.
if (condition) expression1 else expression2 endif
It means that if the condition is true, expression 1 is evaluated; otherwise, expression 2 is evaluated. If you want to do nothing when the condition isn't true, you can omit the part after else. Of course, although it's not a very useful feature, if you want expression 2 to be evaluated when the condition is true, you can replace if with ifn.
Of course, this expression supports more branches, which can be done by adding else if (condition) blocks.
Conditional Loop
The format of a check-before-do loop statement is shown as follows.
while (condition) expression endwhile
It works according to the following mechanism:
- Evaluate the condition expression. If it's true, go to step 2; otherwise, exit the loop.
- Execute the loop body. If a break is encountered, exit the loop. If a continue is encountered, skip the remaining code and go back to step 1. If neither is encountered, after finishing the loop body, go back to step 1.
There is also a check-after-do loop statement:
repeat expression while (condition);
Its operating mechanism is as follows:
- Execute the loop body. If you encounter a break, exit the loop. If you encounter a continue, start over and execute again. If nothing is encountered, after finishing the loop body, go to step 2.
- Check if the condition still holds. If it does, go back to step 1; otherwise, exit the loop.
Although it's also a not very useful feature, if you want the loop body to stop executing when a condition is met, you can change 'while' to 'until'.
Iterative Loop
for (iterator in sequence where condition) expression endfor
This is the basic structure of an iterative loop. Its operation works like this:
- Generate an iterable container based on the sequence, and point the iteration pointer at the container.
- Move the iteration pointer forward by one element. If all elements in the iterable container have been iterated over (the pointer is after the last element of the container), exit the loop. Otherwise, check if the condition in the 'where' clause is met. If it is, go to step 3; if not, repeat step 2.
- Bind the iterator to the iteration pointer and transfer the value, then execute the loop body. If 'break' is encountered, exit the loop. If 'continue' is encountered, go straight to step 2. If nothing is encountered, after executing the loop body, go to step 2 again.
Use iterative loop in iterable items
You can put certain regular elements into a list through iterative loops. For example, the expression below will generate the squares of numbers from 1 to 100.
[x ^ 2 for x in interval(1, 100, 1, true, true) endfor];
Chapter 3: Functions
A function is a combination of some statements. It can encapsulate certain specific functionalities so that each time you want to perform that function, you don't have to make all sorts of changes or copy and paste.
Section 1: Definition
The format for defining a function is shown below.
func FunctionName (args: types, kwargs!: types) returning type do
# Function body
return something;
end
If a function's return type is void, you can skip the return statement in the function.
For example, this function will return "Fizz" when detected 3x, "Buzz" when detected 5x, "FizzBuzz" when detected 15x and the argument as string when detected an integer that is neither divisible by 3 nor by 5.
# fizzbuzz.ppl
func fizzbuzz (x: Int) returning String do
return (x%3=0)?((x%5=0)?"FizzBuzz":"Fizz"):((x%5=0)?"Buzz":cast(x, String));
end
for i in interval(1, 100, 1, true, true) do
print(fizzbuzz(i));
end
And this is a function which greets users with simple sentence.
# greeting.ppl
func greet (user!: Str) returning Void do
print("Hello, %s!" % [user]);
end
greet("Stephen");
Section 2: Calling a Function
As shown above, to cast a function, you write this:
functionName(*args, **kwargs);
Section 3: Recursion and Override
A function can also call other functions inside itself, or even call itself. Calling the function itself from within the function is called recursion. It can usually make the code more intuitive, but it will take up some time and memory.
Programs also allow two functions with the same name but different functions; this is called function overloading.
For example, these are two fibonacci functions for integers and decimals:
include "math".*
let const phi <- (1 + sqrt(5)) / 2;
let const psi <- (1 - sqrt(5)) / 2;
func fibonacci (x: Int) returning Int do
if x <= 1 do
return 1;
else do
return fibonacci(x - 2) + fibonacci(x - 1);
endif
end
func fibonacci (x: Float) returning Float do
return (1 / sqrt(5)) * (phi ^ x - psi ^ x);
end
Among them, the Fibonacci function for integers uses recursion, while the one for floating-point numbers uses the closed-form formula , where phi = and psi = .
The following code snippet defines a foo function that returns a corresponding greeting message if it receives a string, and if it receives a number, it plugs it into the general formula for the Fibonacci sequence and returns the result.
include "math".*
let const phi <- (1 + sqrt(5)) / 2;
let const psi <- (1 - sqrt(5)) / 2;
func foo (x: Number) returning Float do
return (1 / sqrt(5)) * (phi ^ x - psi ^ x); # Both Int and Float are subclasses of Number.
end
func foo (x: Str) returning Str do
return "Hello, %s!" % x;
end
Operator overload
If you want to support an operator that a type does not natively support, you can implement it using operator overloading.
If you need to overload an operator for a type, you can do so by defining a function with the same name as the operator for that type. When an instance of that type uses the operator, the operator function will be called automatically.
The definition of an operator function is similar to that of a regular function, with the following differences:
- When defining an operator function, the
operatormodifier must be added before thefunckeyword; - The number of parameters of the operator function must match the requirements of the corresponding operator (see the appendix on operators for details);
- Operator functions can only be defined inside classes, interfaces, structs, enums, and extensions;
- Operator functions have the semantics of instance member functions, so the
staticmodifier is prohibited; - Operator functions cannot be generic functions.
Additionally, it should be noted that overloading an operator does not change its inherent precedence or associativity.
Section 4: Subroutines
A subroutine is a special type of function that doesn't take any parameters and doesn't return any value (or only returns an exit code). The optional main function we mentioned earlier is a typical example of a subroutine.
For example, this function outputs the FizzBuzz sequence from 1 to 100.
# fizzbuzz.ppl
func fizzbuzz (x: Int) returning String do
return (x%3=0)?((x%5=0)?"FizzBuzz":"Fizz"):((x%5=0)?"Buzz":cast(x, String));
end
func work (Void) returning Void do
for i in interval(1, 100, 1, true, true) do
print(fizzbuzz(i));
end
end
Chapter 4: Lambda Abstractions
Section 1: Definition and Usage
A lambda abstraction is basically an anonymous function.
This type of function is designed to define a feature more quickly in a program without having to give it a name (of course, if you insist on assigning a lambda expression to a variable, that's on you). Here's the definition format for a lambda expression:
{?^ [params: Types, kwparams!: Types] .> {function_body} (real_arguments)}
Here, the part in the bracket parameter list, with multiple parameters separated by commas, and each parameter's name and type separated by a colon. There can also be no parameters inside brackets. The part after .> in the brace is the body of the lambda expression, consisting of a sequence of expressions or declarations. The scope of the lambda expression's parameters is the same as that of a function, and within the body of the lambda expression, its scope level can be considered equivalent to variables defined inside a function body.
Whether a lambda expression has parameters or not, the parameter list and .> cannot be omitted, unless it is used as a trailing lambda.
The type annotation of parameters in a lambda expression can be omitted. In the following situations, if the parameter type is omitted, the compiler will try to infer the type. A compilation error will occur if the compiler cannot infer the type:
- When a lambda expression is assigned to a variable, the parameter type is inferred based on the type of the variable;
- When a lambda expression is used as an argument in a function call, the parameter type is inferred based on the type of the corresponding function parameter.
Section 2: Closure
A function or lambda that captures a variable from the static scope in which it is defined is called a closure, together with the captured variable. This allows the closure to function correctly even when it is used outside the scope where it was defined.
Accessing the following types of variables in the definition of a function or lambda is called variable capture:
- Accessing a local variable defined outside the function in the default value of a function parameter;
- Accessing a local variable defined outside the function or lambda within the function or lambda;
- A function or lambda defined inside a class/struct that is not a member function accessing instance member variables or this.
The following types of variable access are not considered variable capture:
- Accessing a local variable defined within the function or lambda itself;
- Accessing the function or lambda parameters;
- Accessing global variables and static member variables;
- Accessing instance member variables within instance member functions or properties. Since instance member functions or properties receive this as a parameter, all instance member variables are accessed through this.
Variable capture occurs at the time the closure is defined, so there are the following rules for variable capture:
- The captured variable must be visible at the time the closure is defined, otherwise a compile error occurs;
- The captured variable must be fully initialized at the time the closure is defined, otherwise a compile error occurs.
To prevent closures that capture variables that have already defined by "let" from escaping, such closures can only be invoked and cannot be used as first-class citizens. This includes not being able to be assigned, in argument list, returned, or expressionized.
Section 3: Trailing lambdas which borrowed from CangjieLang and Gemini
Trailing lambdas can make function calls look like built-in language syntax, increasing the extensibility of the language.
When the last parameter of a function is of function type, and the argument provided for the function call is a lambda, you can use trailing lambda syntax to place the lambda at the end of the function call, outside the parentheses.
For example, in the code below, a myIf function is defined, where the first parameter is of type Bool and the second parameter is a function type. When the first parameter is true, it returns the result of calling the second parameter; otherwise, it returns 0. When calling myIf, you can either call it like a regular function or use the trailing lambda style.
func myIf (something: Bool, fn: lambda .> (Void)) returning Void do
if something do
fn();
else do
nop;
endif
end
func test (Void) returning Void do
myIf(1+1=2, {?.>{print("Hello, World!");}});
myIf(1+1=2) do
print("Hello, World!")
end
end
Chapter 5: User-defined Data Types
In addition to built-in data types, users can also create their own data types. These custom data types are called structures. The structures mentioned here are a concept found in most languages.
Section 1: Definition of Structures
Structures are defined by structure keyword.
structure YourStructName inside
let attrib (Type) [<- value]; # Attribution definition
public func __init__ (initialization_arguments) returning Void do
# Initialization
end
public func method (args: Types, kwargs!: Types) returning Type do
# Methods
end
end
This is the syntax of defining a structure. For example, this is a structure of Ellipsis:
include "math".*
structure Ellipsis inside
let major (Float);
let minor (Float);
public func __init__ (a: Float, b: Float) returning Void do
this.major <- a;
this.minor <- b;
end
public func area (Void) returning Float do
return pi * a * b
# The formula for the area of an ellipse is S = πab,
# where a and b are the lengths of the ellipse's semi-major
# and semi-minor axes, respectively.
end
end
Struct member variables are divided into instance member variables and static member variables (modified with the static keyword). The difference in access is that instance member variables can only be accessed through a struct instance (saying that a is an instance of type T means that a is a value of type T), while static member variables can only be accessed through the struct type name.
When defining instance member variables, you may omit an initial value (but the type must be specified, like width and height in the example above), or you can provide an initial value.
Section 2: Instances of Structures
After defining a struct type, you can create an instance of the struct by calling its constructor. Outside of the struct definition, you can create an instance of this type by calling the constructor with the struct type name, and you can access instance member variables and instance member functions that meet the visibility modifiers (such as public) through the instance. If you want to modify the values of member variables through a struct instance, you need to define the struct variable as mutable, and the member variables to be modified must also be mutable. When assigning or passing as a parameter, the struct instance will be copied (if the member variable is a reference type, only the reference is copied, not the object it refers to), creating a new instance, and modifications to one instance will not affect the other instance.
A struct supports defining a static initializer, and in the static initializer, static member variables can be initialized through assignment expressions.
The static initializer begins with the keyword combination static func __init__, followed by an empty parameter list and a function body, and it cannot be modified by access modifiers. All uninitialized static member variables must be initialized in the function body, otherwise a compilation error will occur.
Structs support two types of constructors: regular constructors and primary constructors.
A regular constructor begins with the keyword init, followed by a parameter list and a function body. In the function body, all uninitialized instance member variables must be initialized (if the parameter name and member variable name cannot be distinguished, you can use "this" before the member variable to differentiate; "this" refers to the current instance of the struct), otherwise, a compilation error will occur.
In addition to defining multiple regular constructors named init, a struct can also define (at most) one primary constructor. The primary constructor has the same name as the struct type, and the parameters can be in two forms: regular parameters and member variable parameters (you need to "define the parameters as variables"). Member variable parameters simultaneously serve to define member variables and as constructor parameters.
If a struct definition does not have any custom constructors (including primary constructors), and all instance member variables have initial values, a parameterless constructor will be automatically generated (calling this constructor will create an object where all instance member variables have values equal to their initial values); otherwise, this parameterless constructor will not be automatically generated.
Struct member functions are divided into instance member functions and static member functions (modified with the static keyword). The difference between the two is that instance member functions can only be accessed through a struct instance, whereas static member functions can only be accessed through the struct type name. Static member functions cannot access instance member variables or call instance member functions, but instance member functions can access static member variables and static member functions.
Members of a struct (including member variables, member properties, constructors, member functions, and operator functions) are modified with four access modifiers: private, internal, protected, and public, with the default modifier being internal.
- private means visible within the struct definition.
- local means visible only within the current package and its subpackages (including subpackages of subpackages).
- global means visible within the current module (program).
- public means visible both inside and outside the module.
Structs defined recursively or mutually recursively are both illegal.
Chapter 6: Structures VS Classes
Of course, besides structs, you can also define a custom data type using a class. The powerful thing about classes compared to structs is that they support inheritance and polymorphism, and they are reference types.
Section 1: Terminology
- Class
- Used to describe a collection of objects that have the same attributes and methods. It defines the attributes and methods shared by each object in the collection. An object is an instance of a class.
- Method
- Functions defined within a class.
- Class variable
- Class variables are shared among all instantiated objects. Class variables are defined inside the class but outside of any function body. ; Class variables are usually not used as instance variables.
- Data member
- Class variables or instance variables used to handle data related to the class and its instances.
- Method overriding
- If a method inherited from a parent class does not meet the needs of the subclass, it can be modified. This process is called method overriding, also known as method overwrite.
- Local variable
- Variables defined within a method, only applicable to the current instance of the class.
- Instance variable
- In a class declaration, properties represented by variables are called instance variables. An instance variable is a variable prefixed with self.
- Inheritance
- When a derived class inherits the fields and methods of a base class. Inheritance also allows an object of a derived class to be treated as an object of the base class. For example, with a design where a Dog object is derived from the Animal class, this simulates an "is-a" relationship (for example, Dog is an Animal).
- Instantiation
- Creating an instance of a class, a concrete object of the class.
- Object
- An instance of a data structure defined by a class. An object includes two types of data members (class variables and instance variables) and methods.
Section 2: Definition
The definition of a class is quite similar to that of a struct — a name, some properties, a constructor, and some other methods. For example, the following code rewrites the ellipse struct defined in the previous chapter as a class:
include "math".*
class Ellipsis {
let major (Float);
let minor (Float);
public static func __init__ (a: Float, b: Float) returning Void do
this.major <- a;
this.minor <- b;
end
public func area (Void) returning Float do
return pi * this.major * this.minor;
# The formula for the area of an ellipse is S = πab,
# where a and b are the lengths of the ellipse's semi-major
# and semi-minor axes, respectively.
end
}
See? Very similar. It's not because I'm lazy, it's just that structures really do have things in common with classes.
A class modified with abstract is an abstract class. Unlike a regular class, in an abstract class, you can define regular functions as well as declare abstract functions (without a function body). The open modifier is optional when defining an abstract class, and you can also use the sealed modifier to declare an abstract class, indicating that it can only be inherited within the same package.
After defining a class type, you can create an object by calling its constructor (using the class type name to call the constructor). Once an object is created, you can access its instance member variables and instance member functions (that are declared public) through the object. If you want to modify the values of member variables through the object (which is not recommended; it's better to modify them through member functions), you need to define the member variables in the class as mutable (i.e., defined with no mutable-modifier or with var). Unlike structures, when objects are assigned or passed as parameters, the object is not copied. Multiple variables point to the same object, so modifying a member variable through one variable will also change the corresponding member variable in the other variables.
Section 3: Inheriting and Polymorphism
include "math".*
class Ellipsis {
let major (Float);
let minor (Float);
public static func __init__ (a: Float, b: Float) returning Void do
this.major <- a;
this.minor <- b;
end
public func area (Void) returning Float do
return pi * this.major * this.minor;
# The formula for the area of an ellipse is S = πab,
# where a and b are the lengths of the ellipse's semi-major
# and semi-minor axes, respectively.
end
}
class Circle inheriting Ellipsis {
let radius (Float);
public override static func __init__ (r: Float) returning Void do
this.radius <- r;
end
public override func area (Void) returning Float do
return pi * this.radius ^ 2;
end
}
In the code above, we defined an Ellipse class, which has a semi-major axis and a semi-minor axis, and the area formula is S = πab. Then we defined a Circle class through inheritance. Since it's a special type of ellipse, we let it inherit the ellipse's methods, but because of its uniqueness, we also overloaded two of its methods. First, we combined the semi-major axis and semi-minor axis into a radius, so the __init__ function only needs to initialize the radius. Second, when the two factors are equal, their product equals the square of one of the factors, so we overloaded the area function to return πr² (the formula for the area of a circle).
Section 4: Interfaces
An interface is used to define an abstract type. It does not contain data but can define the behavior of a type. A type that declares it implements an interface and implements all the members of that interface is said to have implemented the interface.
Members of an interface can include:
- Member functions
- Operator overload functions
- Member properties
These members are all abstract, requiring the implementing type to have corresponding member implementations.
A simple interface is defined as follows:
interface I { # 'open' modifier is optional.
func f(): Unit
}
Interfaces are declared using the keyword 'interface', followed by the interface identifier I and the interface members. Interface members can be modified with the 'open' keyword, and the 'open' modifier is optional.
Once an interface I declares a member function f, any type that implements I must provide a corresponding f function.
Since interfaces are open by default, the 'open' modifier in the interface definition is optional.
Example: Shape Family
include "math".*
interface Shape {
public static func __init__ () returning Void do end
public func area (Void) returning Float do end
}
class Ellipsis implementing Shape {
let major (Float);
let minor (Float);
public static func __init__ (a: Float, b: Float) returning Void do
this.major <- a;
this.minor <- b;
end
public func area (Void) returning Float do
return pi * this.major * this.minor;
# The formula for the area of an ellipse is S = πab,
# where a and b are the lengths of the ellipse's semi-major
# and semi-minor axes, respectively.
end
}
class Circle inheriting Ellipsis implementing Shape {
let radius (Float);
public override static func __init__ (r: Float) returning Void do
this.radius <- r;
end
public override func area (Void) returning Float do
return pi * this.radius ^ 2;
end
}
class Rectangle implementing Shape {
let width (Float);
let height (Float);
public static func __init__ (a: Float, b: Float) returning Void do
this.width <- a;
this.height <- b;
end
public func area (Void) returning Float do
return this.width * this.height;
end
}
class Square inheriting Rectangle implementing Shape {
let size (Float);
public override static func __init__ (a: Float) returning Void do
this.size <- a;
end
public override func area (Void) returning Float do
return this.size ^ 2;
end
}
An interface can also use the sealed modifier to indicate that it can only be inherited, implemented, or extended within the package where the interface is defined. Sealed already implies the semantics of public/open, so if you provide public/open modifiers when defining a sealed interface, the compiler will issue a warning. Subinterfaces that inherit a sealed interface or abstract classes that implement a sealed interface can still be marked as sealed or not use the sealed modifier. If a subinterface of a sealed interface is marked as public and is not sealed, then its subinterfaces can be inherited, implemented, or extended outside the package. Types that inherit or implement a sealed interface do not need to be marked as public.
There is a built-in interface called the Everything type. All interfaces by default inherit from Everything, and all non-interface types implement the functionality of Everything. Therefore, all types are subtypes of Everything.
Volume 2: Working on Big Projects
Coming Soon.
Appendix 1: Examples
Coming Soon.