Primer

Be you a grizzled game development veteran or new to the world of programming, this guide will get you started with SkookumScript and touch on all aspects of the language and its tool suite.

Once you are done this Primer, you will have a decent enough grasp of SkookumScript to be ready to experiment and be dangerous. Enjoy!

  1. Initial installation
    1. Start the Runtime and the SkookumIDE
  2. Learn the Workbench basics
  3. SkookumScript the language
  4. Comments
  5. Expressions
  6. Literals
    1. Integer
      1. Specifying a radix (or base)
      2. Digit grouping separator
    2. Real
      1. E-notation
      2. Expected number class types
    3. Boolean (true and false)
      1. Converting to and from Boolean values
    4. String
      1. Escaped special characters
    5. Symbol
    6. List
    7. Closure: the basics
  7. Identifiers
    1. Variables
      1. Initial binding
      2. Rebinding
    2. Class Identifiers
    3. Reserved Identifiers
    4. Data members
      1. Instance data
      2. Class data
    5. Object IDs
      1. Reference @
      2. Potential Reference @?
      3. Identifier @#
  8. Invocations
    1. Calling routines and passing arguments
      1. Invocations with no arguments
      2. Invocations with arguments
      3. Group Arguments
      4. Default parameters and omitting arguments
      5. Named arguments
      6. Return arguments
      7. Closures as arguments
        1. Closure tail arguments
    2. Binary and unary operator calls
      1. Assignment operator :=
        1. When to use assignment := or binding :
      2. Math operators
        1. Subtract operator vs. negative number or negated
      3. Comparison operators
      4. Boolean operators
        1. Short-circuit evaluation
    3. Instantiation (creating new objects)
      1. Expression Instantiation
        1. Modified copies
    4. Index operator {idx}
      1. Index set operator {idx}:
    5. Apply operator %
      1. List apply
      2. Non-list apply
    6. Closure invoke operator
    7. Conversion calls
  9. Type Primitives
    1. Class Conversion >>
      1. Redundant conversion
      2. Inferred conversion class
      3. Conversion variations
    2. Class Cast <>
      1. Downcasting
      2. Reduce Casting
        1. Casting off nil/None
        2. Combined Downcast and Reduce Casts
      3. Illegal Casts
      4. Inferred Casting

Initial installation

If you haven’t done so already, now is the time for you to install either the Stand-alone SkookumDemo (a fast-and-easy install that allows to you check out SkookumScript without installing a game engine or world editor) or the SkookumScript Unreal Engine 4 Plugin and SkookumDemo project (a longer and more involved setup that also includes setting up Unreal Engine 4 and using its world editor).

NOTE Both the SkookumScript UE4 Plugin and UE4 itself can be downloaded as pre-built binaries, or you can build them yourself, which enables you to pull them apart and put them back together according to your liking even with different game engines, as well as contribute fixes and improvements.

Start the Runtime and the SkookumIDE

Ensure that that the stand-alone demo or the UE4 Editor is up. These are referred to as the runtime.

If you are using the stand-alone-demo, it will automatically launch and connect to the SkookumIDE.

If you are using the UE4 Editor, make sure you have a project loaded – we recommend the SkookumDemo project. To launch the SkookumIDE (if it isn’t already up and connected) press the Sk button on the UE4 editor toolbar.

UE4 SkookumIDE disconnected UE4 SkookumIDE connected

The UE4 Editor SkookumIDE connection toolbar button indicating disconnected with a red no symbol at the bottom right and just the regular SkookumScript Sk icon if connected respectively.

SkookumIDE disconnected SkookumIDE connected

The SkookumIDE runtime connection toolbar button indicating disconnected with a red no symbol at the bottom right and just the regular SkookumScript Sk icon if connected respectively.

NOTE When using the UE4 Editor, many types of SkookumScript code can only be executed when your project is running – for example any commands that reference game objects or commands that take time to complete. No game objects are created/available when just in UE4 editor mode.

So if it isn’t already running, start your UE4 project. Press the “Play” icon on the UE4 toolbar or press Alt+P in the UE4 Editor to “Play in the editor (PIE)”.

Play button on the UE4 Editor toolbar

To get mouse control when the game has captured it and you want to use the mouse to click on the Skookum IDE, etc. – press Shift+F1 in the Unreal Editor.

NOTE The SkookumIDE is an independent application. It can be used to edit scripts without being connected to the runtime so it stays up even after the runtime (stand-alone-demo or UE4 Editor) is closed. If it is still up when you reload the stand-alone-demo or the UE4 Editor, it will just reconnect. If you don’t need it after you shutdown the runtime, you can just close it.

TIP The SkookumScript UE4 Plugin remembers the SkookumIDE state from the last time you launched the UE4 Editor. If the SkookumIDE was up when you shut the UE4 Editor down, the next time you run the UE4 Editor, it will automatically launch and connect to the SkookumIDE. If the UE4 Editor loses its connection with the SkookumIDE for any any reason, just press the Sk button on the UE4 toolbar to launch the SkookumIDE and connect. This will also reconnect to the SkookumIDE if it is already running though not connected to the runtime. If the SkookumIDE is already running and already connected to the UE4 Editor then pressing the Sk button will bring the SkookumIDE to the foreground.

Learn the Workbench basics

First, check out the Workbench widgets which are command consoles in the SkookumIDE that are used to evaluate code snippets on a live running project. Workbenches will be used extensively throughout this Primer.

In particular, make sure to go over the Workbench basics including Printing to the Log and Parser error checking. You can also skip ahead of this Primer a bit and go through all the examples in the Tutorial Workbench. Then come back and continue this Primer.

SkookumScript the language

SkookumScript is a new language. Even though many of its concepts and constructs are pulled from existing languages, it is a unique creation with unusual elements that make effective SkookumScript code different in character from code written in its relatives. Straightforward translation of code from a different language to SkookumScript is unlikely to capture the full potential and delight of SkookumScript code written with its strengths and idioms in mind. Once you have a full grasp of SkookumScript, you are likely to come at solutions to problems from directions you didn’t previously even know existed.

First and foremost, SkookumScript is designed for gameplay logic and interactive concurrent time-flow. Everything about SkookumScript originates from this core goal including many custom constructs built right into the language.

SkookumScript is a true object-oriented embedded programming language which is compiled and efficient. It is statically type-checked with many types inferred and the parser doing as much work for you as it can. The syntax aims to be light and brief with a high signal to noise ratio—to get a lot done in a small amount of code. SkookumScript also takes into account that it isn’t the only tool in your toolbox so it is designed to be complimentary to existing tools (such as world editors and other coding environments), development pipelines and languages (such as C++ and UE4 Blueprints).

SkookumScript looks different from other languages in a few key ways, such as its use of square brackets [ ] for code blocks, and its use of whitespace between expressions, arguments and list elements instead of special characters or the end of the line. This may make things interesting as you first start to learn the language, though any differences will fade away as you quickly pick up SkookumScript and spend more and more time on what you want to create rather than how you want to create it.

SkookumScript has been developed over many years on numerous projects and studios from little indie games to award winning AAA titles.

We on the SkookumScript team promise that SkookumScript will change the way that you develop and you’ll wonder how you got by without it in the past.

So now that we are done with the intro and the pleasantries, here follows a step-by-step description of the SkookumScript language.

Comments

Before we go deep in the Primer, let’s cover comments since they will be used in many of the examples.

Comments in SkookumScript are similar to C++:

Single-line comments begin at any point on a line with two forward slashes // and it continues until the end of the line.

// This is an example of a single line comment.
// It continues from its start until the end of the line ->
do_this

// It can be on its own line
do_that  // or at the end of other code

Multi-line comments start with a forward slash and then an asterisk /* and they run until their end which is an asterisk and a slash */ – the reverse of the start. Unlike C++ multi-line comments, SkookumScript comments can be nested inside of other multiline comments. This enables commenting out large swaths of code quickly even if the code already has multi-line comments.

/* This is an example of a multi-line comment.
   It can be a single line, multiple lines or used
   in the middle of code just like other whitespace. */

if /*like this*/ test?
  [
  do_this
  do_that
  ]

/* [ML1] Multi-line comments
do_this 

  /* [ML2] can nest other comment blocks
  do_that
  do_stuff // Including single line comments
  */  // end of [ML2]

do_other
*/  // end of [ML1]
do_end

No matter how deeply immersed you are in an area now you can be sure that the mists of time will clear much of it away.

// You may look at this code and think that rewriting it
// to do X is a good idea - don't do it! Y and Z limit
// the solution to the way it is written here.
// Modify at your peril!

Thanks, past me! I forgot and was about to rewrite this code.

Future you

I know, this is the second time we thought to rewrite this code. The first time there was no comment so we forgot the peril and wasted a bunch of time.

Past you

See the comments reference for details on the comment syntax.

Expressions

An expression is a combination of one or more specific commands, queries, values or names that is computed sometimes with side-effects to produce a result value. Expressions are the smallest elements that describe an action in SkookumScript.

Performing the action and doing whatever computation is necessary is called code evaluation or often code execution. In SkookumScript, everything is an expression and all expressions return a result when evaluated.

Simple whitespace (which includes comments) is used to separate / delimit between expressions—or even no whitespace if the separation is obvious to the parser. Expressions do not need an end of expression / statement delimiter symbol (such as ;, . or newline)—the SkookumScript parser knows what is and is not a valid expression and it determines where one expression ends and another begins.

Same expressions with different spacing
1
2
3
4
5
6
7
8
9
println("expr1") println("expr2") println("expr3")

println("expr1")
println("expr2")
println("expr3")

println("expr1")
  println("expr2")
    println("expr3")

TIP Multiple expressions can be grouped together and treated as a single expression using a code block.

There several kinds of expressions:

Literals

A literal is a way of writing a specific value for a new object you want to create.

SkookumScript has several different types of literals:

  • Integer (42, -123, 123_456_789, 2r10101, 8r52, 0x1b)
  • Real (0.0, 3.14159, -.33, 7.5e-8)
  • Boolean (true, false)
  • String ("Hello, world!", "first" + " second", "Line 1\nLine2")
  • Symbol ('idle', 'playing bongo', 'eating sandwich')
  • List ({1 2 3}, {1 "two" 3.0}, String{})
  • Closure (()[do_stuff], 10.do[do_stuff])

Integer

You can’t get far in a programming language without numbers.

Integers are whole numbers without a fractional part and they can be positive, zero or negative.

42
0
-123

NOTE The placement of the minus - symbol determines whether it applies to a negative integer literal -42, a negated operator -num or a subtract operator num - 42—see Subtract operator vs. negative number or negated.

See the full Integer syntax here.

Also check out the Integer class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment, math and comparison.

Specifying a radix (or base)

These integers are all in decimal which is radix (or base) 10.

In programming, it is often handy to be able to use a different radix than than the default radix 10. To specify a particular radix, prefix the radix you want to use followed by an r and then digits or letters that are valid for that radix that represent the number you want.

// These all are the same as:
42       // Decimal

2r10101  // Binary
8r52     // Octal
10r42    // Decimal - redundant though valid
16r2a    // Hexadecimal
21r2     // radix 21 - sure why not?
36r16    // Haxatrigesimal - largest allowed radix

Digit grouping separator

When you have long numbers, it can be handy to mark groups of thousands (or however you want) so you don’t get lost in digits. SkookumScript allows you to do this with optional underscores _ between digits.

123_456_789

The underscore is a common digit separator used by several languages including C#, Swift, Ruby, Python, Java, Perl, Ada and Eiffel. Space, comma , or single quote ' cannot be used to mark groupings since they are used for other language syntax elements.

Real

Real literals are used to create numbers that include a fractional part (real numbers) and they are represented by objects of the class type Real. (Real objects use the traditional floating-point mechanism shared with similar types often known as float or double in other languages such C++.)

4.2e1    //         42
1e6      //  1 000 000
-1e6     // -1 000 000
7.5e-8   //  0.000 000 075
-7.5e-8  // -0.000 000 075

See the full Real syntax here.

Also check out the Real class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment, math and comparison.

E-notation

Real values can also be written using E-notation as a shorthand for Scientific notation (m * 10n).

E-notation: real value e power of ten

4.2e1    //         42
1e6      //  1 000 000
-1e6     // -1 000 000
7.5e-8   //  0.000 000 075
-7.5e-8  // -0.000 000 075

Expected number class types

If the parser expects an Integer value to be passed as an argument, then it will not accept a Real value as an argument or vice-versa.

40 + 2.0

This gives the error:

The argument supplied to operator parameter num was expected to be an object of the type Integer and was given the type Real which is not compatible.

SkookumScript does not coerce (automatically convert) from one type to another. (Though you can explicitly convert from an Integer type to a Real type using the conversion operator 2.0>>Real or calling the conversion method 2.0.Integer. We will go into details of conversion later.)

However, if the parser knows that a Real type is expected and if the real value that you want to supply happens to have no fractional part (such as -1.0, 0.0, 1.0, 123.0) then you can omit the .0. So even though it looks like an integer literal, it is actually a real literal (such as -1, 0, 1, 123). This is not coercion—the parser just automatically adds the .0 on for you and treats it as a Real type.

The 2 below is a real literal since the + operator on 40.0, which is a Real type, also expects the operand (the value passed to the operator) to be a Real type.

40.0 + 2

The parser sees the 2 and realizes that what you really mean is 2.0.

It is nice to be lazy when you can. The SkookumScript parser tries to understand what you want and has your back.

Boolean (true and false)

Numbers are great, though what would a programming language be without logic?

SkookumScript has a Boolean literal with two possible values: true and false. They are both of the single class type Boolean.

Boolean values can be used to store logical states and they are especially useful when working with conditional expressions such as if, when and unless that allow changing the control flow of code. More details about conditional expressions can be found here.

Also check out the Boolean class in the SkookumIDE by selecting it using the Classes widget and examine its members in the Members widget. In particular, note the Boolean logical operators and, or, xor, nand, nor, nxor and not that are frequently used with Boolean values and their associated methods.

Converting to and from Boolean values

Unlike some other languages, in SkookumScript only false is accepted as a negative truth value – no other type or value such as 0, nil, empty string or empty list is implicitly accepted or coerced as a false value. Similarly, true is the only accepted positive value – no other values are implicitly accepted as a true value.

If you really need to however, you can explicitly convert from a Boolean type to an Integer type using the conversion operator true>>Integer or calling the conversion method true.Integer – and the reverse 0>>Boolean or 0.Boolean. You can also call obj.not_nil? to convert something that could be nil to false when nil or to true otherwise. Likewise you can call list.empty? or list.filled?.

String

Zero or more characters surrounded by double quotes " is known as a string literal and it creates a String object. The string itself is the characters between the double quotes.

"Hello, world!"

// The empty string
""

Strings can span more than one line, in which case they will store the newline character for the end of each line.

// Multi line string
"line 1
line 2
line 3"

One or more string literals with a concatenation operator + between them will be parsed as a single string. The concatenation operator is necessary since the parser will otherwise consider the adjacent strings to be separate string literals that create separate string objects.

// same as "just one string"
"just " + "one " + "string"

// same as "also one string"
"also "
+ "one "
+ "string"

// Three string objects
"three" "separate" "strings"

String internals

Strings are internally stored as reference counted null-terminated (0) arrays of ANSI characters (to ensure compliance with native C++). Copying from one string to another is quite efficient since they only copy a pointer to a shared string reference structure and increment a reference count.

The internal representation will likely be stored in a Unicode compliant mechanism in the future.

See the full string syntax here.

Also check out the String class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment and comparison.

Escaped special characters

The backslash \ is used to escape (or substitute) special characters in a string (or symbol) literal. The most commonly used special character is probably the newline \n—expect to see it everywhere.

Characters other than the specials of a, b, f, n, r, t and v just pass right through unchanged such as quotes \' \" and the backslash itself \\.

Escape Character substitution Hex
\a bell/alert 07
\b backspace 08
\f formfeed 0c
\n newline 0a
\r carriage return 0d
\t tab 09
\v vertical tab 0b
\integer-literal ANSI character from integer
\? resolves to itself such as \' and \"

When an ANSI character code is specified using \ and an integer literal, it can specify a radix so the number can be in the form easiest for the situation: decimal, hexadecimal, etc.

These all specify the asterisk * character:

"*"          // simple character
"\42"        // binary
"\8r52"      // octal
"\16r2a"     // hexadecimal
"\2r101010"  // binary
"\*"         // pass through

Here is an example that includes double quotes and newlines:

"\"The secret is to bang the rocks together, guys.\"\n"
+ "  - Douglas Adams\n"
+ "  The Hitchhiker's Guide to the Galaxy"

Symbol

Symbols are like a hybrid between strings and enumerations (or integers). They can be just about any sequence of characters like a string though they are as simple and as efficient as enumerations internally.

They look similar to a string though they use single quotes and they are limited to no more than 255 characters.

// identifier names in the language
'some_routine_name'

// used like enumerations
'small'
'medium'
'large'
'fries with that'

// names for behavior states
'idle'
'hunting for smoothie ingredients'
'making smoothie'
'drinking smoothie'
'brain freeze!'

Symbols are what SkookumScript uses to store all of its internal representations for the names of aspects language.

See the full symbol syntax here.

The UE4 equivalent for Symbol is often the C++ FName class which is called the Name class in SkookumScript.

Also check out the Symbol class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment and comparison.

List

Lists group together objects so they can be treated as a single List object. List objects (also sometimes called collections or arrays in other languages depending on their implementation) store zero or more objects referred to as items in an ordered, indexable (they can be retrieved by their position number starting at zero) group that can dynamically change in length. The same object can simultaneously be in several positions in the same list.

A list literal makes a List object using a series of expressions surrounded by curly brackets { } (also known as braces) marking the beginning and the ending of the list. Items are separated by whitespace. You can alternatively separate items with a comma , though they are optional (just as they are with routine arguments) and should only be used if they aid readability.

Here are some simple list literals:

// empty list that expects Object items
{}

// List of 3 integers
{1 2 3}

// Same as above - commas optional
{1, 2, 3}

// List of 3 strings
{"hey" "hi" "hello"}

Each item expression is executed in order and the resulting object is added to the list as an item.

Lists can have more than one item class type.

// List with item type <Integer|String|Real>
{1 "two" 3.0}

Simple lists infer the item class type by examining the items in the list literal.

If you want to have an empty list that will use a particular item class type, the type must be specified using a mechanism such as an item typed list.

// empty list that expects Integer items
Integer{}

// empty list that expects String items
String{}

// Can still specify initial items
Integer{123 42}

// Simpler since Integer is inferred
{123 42}

The syntax of list literals can be simple or tricky depending on the class type of the list, the class type(s) of the items in the list and whether any initial items are present. See the full list reference here for all the details.

Also see some examples of using lists here, check out the List class in the SkookumIDE by selecting it using the Classes widget and look at its powerful and useful members in the Members widget.

Closure: the basics

Closures are a sophisticated and powerful construct that touch on more language concepts than we’ve gone over in the Primer to this point, so for now we’ll only go over their basics. They are a literal so they are included here for completeness, though we’ll detail them later after their building block concepts have been introduced.

A closure literal (also called closures in Swift and known as lambda expressions in C++ or blocks in Ruby and Smalltalk) is essentially a series of expressions grouped together like an anonymous (without a name) routine that also captures (closes around) references to any variables that were created in the surrounding context (outside of the group of expressions). This allows you to treat code as an object so you can pass code to and return from routines and delay its evaluation until needed. This allows callbacks, custom control flow, and all manner of amazing and zany concepts.

Related concepts in other languages go by many names including: lambda functions, anonymous functions, function literals, function objects and delegates. These do not necessarily have all the same features as closures though they are similar.

Some examples of closures:

// Closure that triples an Integer
(Integer x)[3 * x]

// Tail closure called by Integer.do
// The closure is the [ ] bit
10.do
  [
  println("hey")
  println("hi")
  println("hello")
  ]

This is just a taste—don’t worry about having full mastery of closures yet. See Closure invoke operator to learn how to call closures.

See the full closure reference here.

Identifiers

SkookumScript uses names called identifiers to store and refer to values and objects.

SkookumScript is an object-oriented language and all values are objects so the terms value and object may be used interchangeably. We will cover the details of the object-oriented aspects of SkookumScript later in the Primer.

There are several types of identifiers:

  • variables including temporary variables and parameters (value, bad_guy, trigger_region4, success?)
  • class identifiers (Object, String, Integer)
  • reserved identifiers (nil, this, this_class, this_code, this_mind)
  • data members (@hit_points, position.@x, @@random, BefuddledEarthling.@@heard_vogon_poetry_count)
  • object IDs (@'RoboChar1', Enemy@'Bad Bart', BefuddledEartling@?'Arthur', Trigger@#'Trigger by door12')

Variables

A temporary variable (also known as a local variable) is explicitly declared (created) by using an exclamation mark ! followed by the identifier name to call it. This is called a create temporary expression.

!my_variable

This tells the parser that the variable my_variable can be used to reference a value and to remember it.

Think of it like a roll call that shouts (!) a new variable name into existence.

The ! in a SkookumScript create temporary expression is similar to the auto keyword in C++, the var keyword in C#/Javascript/Swift, := in Go or local in Lua. Declare a local temporary variable and implicitly infer the class type.

The variable identifier name must start with a lowercase letter and then any combination of upper and lowercase letters, numbers and underscore characters _. If an identifier name will be referring to a Boolean value (true or false), then the name may also end with a question mark ?—such named variables are called predicate (or alternatively query) variables.

The SkookumScript variable standard naming convention uses snake_case (also known as C naming style) with all lowercase letters and words separated with an underscore character.

var, bad_guy, trigger_region4, success?

Other styles can be used as desired to match the convention of your project or company.

NOTE A create temporary expression may not be used as a send argument though it can be used as a return argument.

Once a variable is created, it is used without the exclamation mark and evaluates to whatever object that it refers to.

!my_variable  // create temporary
my_variable   // evaluate temporary

TIP When you evaluate a temporary variable, it only exists in the scope where it was created—such as the routine or closure. This is also the case for interactively evaluating code snippets in Workbench widgets. The variable from a create temporary will only be remembered by the parser for that one evaluation. If you evaluate a new code snippet afterwards, any previous variables are forgotten. It will also need a create temporary in order to use a variable with the same name and the value of the variable will not be remembered from previous evaluations.

Parameter variables look and act just like temporary variables, though they come from the parameters of a routine or closure.

(String my_parameter)
  [
  // result is string that was passed in as an argument
  my_parameter
  ]

Initial binding

Variables are always bound (refer) to an object. If no object is initially specified to be bound to, then a variable will automatically bind itself to the global nil object.

NOTE In SkookumScript, even simple types such as Integer and Boolean are always stored as objects, and bound by reference. Unlike some languages such as C#, SkookumScript does not store primitive value types and object types differently—everything is an object.

If you evaluate the following create temporary, the result will be nil. Likewise if you evaluate the my_variable variable on its own.

!my_variable  // create temporary and bind to nil
my_variable   // evaluates to nil

The create temporary can also specify an initial object to bind to. To bind an initial object, follow the create temporary with a bind operator (a colon :) and an expression.

!my_variable: "hello"  // create and bind to string
my_variable            // evaluates to "hello"

Most of the symbols used in SkookumScript come from or are inspired from other languages. The use of a colon : to bind a variable with its data originates from key / value pair mappings in YAML, which took it from JSON, which was inspired from REBOL.

The initial object bound to a parameter variable is the argument that is passed into the routine or closure when it is called.

The initial class type of a variable comes from the object that it is bound to.

// Class type is None
!my_variable
// Class type is String
!my_variable: "hello"
// Class type is None
!success?
// Class type is Boolean
!success?: true

The class type is inferred by the SkookumScript parser, it does not need to be explicitly specified.

Rebinding

A variable can be rebound to other objects at any point.

!str: "hello"
println(str)  // prints "hello"
str: "hi"     // change binding to "hi" object
println(str)  // prints "hi"

In the above example, the type of the variable was String and then it was changed to a different object that also had the String type.

A variable can be bound to objects with different types too and the SkookumScript parser automatically keeps track for you.

!answer: "who knows?"
println(answer)  // prints "who knows?"
answer: 42       // change binding to 42
println(str)     // prints 42

In the above example, the type of the variable was String and then it was changed to a different object that had the Integer type.

Predicate variables may only be set to Boolean values. Any attempt to set a predicate variable to a non-Boolean will have a parse error.

!success?: false
println(success?)  // prints false
success?: 42       // Error! Tried to change binding to 42

This will give the error:

Tried to bind to type Integer when Boolean was expected!
Query/predicate temporary variables ending with a question mark may only be bound to a Boolean true/false expression.

NOTE A bind expression may not be used as an argument—only a named argument may be used which looks similar.

Class Identifiers

Class identifiers are just the name of a class and the result of their evaluation is a class object. Class objects can be stored in variables and passed around just like any other object. Every class in SkookumScript is unique and globally accessible.

// The Object class which is the root of all classes
Object

// The String class
String

// The Debug class 
Debug

A class name must start with an uppercase letter and then any combination of upper and lowercase letters, numbers and underscore characters _.

The SkookumScript class standard naming convention uses PascalCase (also known as upper camel case) with each word capitalized and all other letters lowercase. Special classes used for generic programming (types to be specified later) end with an underscore.

String, InvokedBase, AIPerceptionSystem, Vector3, ThisClass_

Other styles can be used as desired to match the convention of your project or company.

Reserved Identifiers

Reserved identifiers are built-in names that refer to common objects. They look like regular variables and are used similarly too though they are always present and cannot be used for identifier names for temporary variables or parameter names.

  • nil: the single instance of the None class. It is used to represent nothing or the absence of a value. (It goes by several different names in other languages, such as NULL (C), nullptr (C++), and null (C#/JavaScript).)
  • this: the current / reciever object that is running the current code. (Some languages use the identifier self or Me.)
  • this_class: the class of the current object.
  • this_code: the invoked code object that represents the currently running code. Useful for metaprogramming. See the InvokedBase class, its subclasses and its members in the SkookumIDE to see the calls that can be made on it.
  • this_mind: the mind object that is updating the current routine. See Mind objects and the Master Mind.

Data members

Data members are subcomponents of objects (known as data structures in non-object-oriented languages) that are essentially variable identifiers that belong to an object.

We haven’t gone into much of the details of SkookumScript object-oriented programming at this point in the Primer, so we’ll just mention how data members are used rather than how they are created.

Instance data

Object (or class instance) data members belong to a single object and start with an a single at symbol @ followed by the same characters used for temporary variables: @x, @hit_points, @heard_vogon_poetry?

All instance data members are part of an object and you can specify their owner object with an expression followed by a dot: position.@x, this.@hit_points, arthur.@heard_vogon_poetry?

If you want to use the object from the current scope, it can be omitted and this. will be inferred. So @hit_points is the same as this.@hit_points.

Class data

Class data members belong to a whole class of objects including the class object itself and start with two at symbols @@ followed by the same characters used for temporary variables: @@random, @@hit_point_max, @@heard_vogon_poetry_count

All class data members are part of a class and you can specify their owner class with an expression followed by a dot: Object.@@random, this_class.@@hit_point_max, BefuddledEarthling.@@heard_vogon_poetry_count

If you want to use the class from the current scope, it can be omitted and this_class. will be inferred. So @@max_hit_points is the same as this_class.@@max_hit_points.

All instance objects have access to their class data members too. Class data members can be referenced from instance objects: guy.@@hit_point_max, arthur.@@heard_vogon_poetry_count?. Class data members can also be accessed directly from within instance routines: @@hit_point_max, @@heard_vogon_poetry_count?

See the Data Members reference page to learn more about data members including their declaration, initialization and use.

Object IDs

Object IDs are a feature unique to SkookumScript—they allow game / simulation objects, concepts and assets to be referred to by a combination of class type and object name in code.

Object IDs can be used for game characters, trigger regions, sound effects, user interface elements, mission names, cars, cutscenes, props, doors, geographical locations, art assets, game events, AI / behaviour types, animations or anything that you can associate a name to.

Object IDs start with an optional class name, next an at symbol @ (and a question mark ? for a potential reference or a hash mark # for an object ID identifier) then a symbol literal for the lookup name of the object.

Also see the full Object ID reference.

Reference @

An object ID using @ returns the named object and has a runtime error if the object cannot be found.

// Actor class inferred with name "RoboChar1"
@'RoboChar1'

// Same as above – Actor named "RoboChar1"
Actor@'RoboChar1'

// Specific class type Enemy
Enemy@'RoboChar1'

Potential Reference @?

An object ID using @? returns the named object or nil if the object cannot be found.

// Returns nil if not found
Enemy@?'RoboChar42'

NOTE Since this could be either an Enemy object or nil, the class type is a union class <Enemy|None> which we will go over in more detail later in the Primer.

Identifier @#

An object ID using @# returns the identifier for a named object in the type most appropriate for the runtime.

// Returns 'RoboChar42' as a Name object (for UE4)
Enemy@#'RoboChar42'

The UE4 identifier class type is Name in SkookumScript and Blueprints (FName in C++), so that is the result type for object IDs when using UE4.

So Enemy@?'RoboChar42' is essentially the same as 'RoboChar42'.Name

Invocations

Coding is about as close as you can get to actual magic—where the exertion of imagination and the arrangement of the correct wording brings pure thought-stuff into being. And like magic, the lifeblood of coding is invocations.

invocation (noun)

  1. Magic. the incantation or formula used to conjure up a magical entity for aid, protection, inspiration, or the like
  2. Computing. a subroutine call

An invocation or invoking a routine is also known as running, calling or executing a routine. Routines are a sequence of one or more expressions that perform a specific task, packaged as a unit.

Like any expression, all routines return a result object.

SkookumScript has two types of routines:

  • methods: routines that are immediate (essentially instant and take no time) returning within the same update frame.

    Method names are similar to variable identifier names—they must start with a lowercase letter and then any combination of upper and lowercase letters, numbers and underscore characters _.

    Methods can return any type of object. If a method has a Boolean (true or false) result, then the method name may also end with a question mark ?—such methods are called predicate (or query) methods.

    The SkookumScript method naming convention uses snake_case (also known as C naming style) with all lowercase letters and words separated with an underscore character.

    print, path_stop, some_long_method_name, empty, empty?

    Other styles can be used as desired to match the convention of your project or company.

    There are also special methods that have exceptions to this naming including constructors (start with a single exclamation mark ! then optional identifier: !, !copy, !xyz), destructors (two exclamation marks !!), conversion calls (same name as the class being converted to: String, Integer, Name) and operator calls (+, *=, =, >=, --). See the method name syntax and “Adding a new routine” in the New Class or Member pane.

  • coroutines: routines that are durational (may take time) returning either within the same update frame (like a method) or one or more update frames later.

    Coroutine names are similar to method names though they must start with an underscore _ and then like a method, a lowercase letter and then any combination of upper and lowercase letters, numbers and underscore characters.

    TIP The initial underscore helps make calls that take time stand out in SkookumScript code.

    The SkookumScript coroutine naming convention uses an initial underscore then snake_case with all lowercase letters and words separated with an underscore character.

    _do, _wait, _play_sound, _path_to_actor

    Other styles can be used as desired to match the convention of your project or company.

    Invoking a coroutine results in an InvokedCoroutine object which stores the state of a (running or completed) coroutine. Manipulating such objects is fairly advanced so we won’t get into their details here.

Calling routines and passing arguments

To Invoke or call a routine, specify the name of the routine to call followed by any arguments that you want to pass along to the routine in parentheses ( ). If there are no arguments to pass along, then the parentheses are optional.

Invocations with no arguments

Some pseudocode showing invocations without any arguments.

// no arguments
method_name()
_coroutine_name()
 
// Same as above - () are optional
method_name
_coroutine_name

Remember that all routine calls are on objects, so the above are the same as:

// Calls a method with no arguments
this.method_name()
this._coroutine_name
 
// Same as above - () are optional
this.method_name
this._coroutine_name

They could be called on other objects too:

// Calls a method with no arguments
object.method_name()
some_guy._coroutine_name
 
// Same as above - () are optional
object.method_name
some_guy._coroutine_name

Here are some actual zero argument calls. You can evaluate their individual lines in the Workbench widgets in the SkookumIDE.

// Determines if an object is the nil object
nil?
 
// Randomly return true or false
@@random.coin_toss
 
// Binary version of number in string form
42.binary
 
// Computes the Fibonacci number
42.fibonacci
 
// Get the length of a list
{1 2 3}.length
 
// Get the transform of the player
player_pawn.transform
 
// Get transform of RoboChar1 in the SkookumDemo project
Enemy@'RoboChar1'.transform

Invocations with arguments

As an argument expression is encountered, it is evaluated and its resulting object is passed by reference to the routine—only the memory address of the object is passed to the routine rather than a new copy of the object. The class type of the argument result object must be compatible with the desired type of the routine’s parameter matching that argument.

Some pseudocode showing invocations with a single argument:

// 1 argument
method_name(arg1)
_coroutine_name(arg1)

Some actual single argument calls:

// Generates a random integer between 0 and 9
@@random.uniform_int(10)
 
// Returns max of two integers - obviously 42 here
3.max(42)
 
// Wait 2 seconds
_wait(2)
 
// Instruct RoboChar1 to path towards the player
Enemy@'RoboChar1'._path_to_actor(player_pawn)

If an argument passed to a routine is not a match for the expected type of the routine, you will get a parser error.

_wait("hello")

This will have the parser error:

The argument supplied to parameter named seconds was expected to be of type Real and it is type String which is not compatible.

Some routines take two or more arguments which are separated with whitespace. A combination of whitespace and a comma may also be used to separate arguments.

SkookumScript standard style is to prefer whitespace separation over commas unless the use of commas is more readable in a particular context.

Some pseudocode showing invocations with two arguments:

// 2 arguments
// Note: commas are not needed
method_name(arg1 arg2)
_coroutine_name(arg1 arg2)
 
// Commas are optional
method_name(arg1, arg2)
_coroutine_name(arg1, arg2)

Arguments are evaluated in the order in which they are encountered in the code—left to right and top to bottom. So in the examples above, the arg1 expressions will always be evaluated before the arg2 expressions.

// Generates a random real number between .2 and 42.0
@@random.uniform_range(.2 42)
 
// Determines if 42 is between 5 and 103: true
42.between?(5 103)
 
// Append 42 to the list 3 times: {1 2 42 42 42}
{1 2}.append_items(42 3)
 
// Swap objects at indexes 0 and 2: {"hi" "hello" "hey" "howdy"}
{"hey" "hello" "hi" "howdy"}.swap(0 2)

Routines may have any amount of arguments from zero to hundreds, though more than a few can be unwieldy.

Some pseudocode showing invocations with three and more arguments:

method_name(arg1 arg2 arg3)
method_name(arg1 arg2 arg3 arg4 arg5 arg6 arg7)
method_name(arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10)
 
// The whitespace between arguments can span lines for readability
method_name(
  arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10)
 
method_name(
  arg1
  arg2
  arg3)

Group Arguments

Routines can have group parameters which allow zero or more arguments to be passed into that single parameter. The arguments are stored in a List with the parameter name when they are used in the code bode of the routine. Arguments that match a group parameter are called group arguments. A call to a routine with group parameters will accept as many group arguments to that parameter once the first argument in that index position matches the expected class type and all the arguments following will also be grouped together until there are no more arguments or there is an argument that does not match.

A good example of a routine with group arguments is the println() method that we have already been using. The println() method takes any number of arguments, converts them all to strings, concatenates them all together, adds a newline and then prints it all out.

// 1 argument - print some string variable
println(str)
 
// 2 arguments
println("This is the string: " str)
 
// 3 arguments - wrap it in quotes
println("This is the string: \"" str "\"")
 
// 9 arguments
println(
  "This is string1: \"" str1 "\"\n"
  "This is string2: \"" str2 "\"\n"
  "This is string3: \"" str3 "\"")

We aren’t covering routine parameters yet, though here is what the parameters for println(), which uses a group parameter, looks like:

({Object} objs_as_strs)

It shows that it expects zero or more arguments of the Object class (all classes match with Object since it is the superclass of all classes) and to put them all in a List object named objs_as_strs.

Also see the List methods appending(), assigning(), inserting(), intersecting_same(), matching_same(), removing_same(), unioning_same() which all use group arguments.

Default parameters and omitting arguments

Each parameter in a routine can have a default argument expression. Any such parameter can have an argument passed to it like normal or it can be omitted and the default argument will be used instead. This is useful for arguments that often have a common value that would be used. Default values are also used for niggly detail arguments that rarely need to be specified.

For example, the _wait() coroutine has a seconds parameter that has a default argument of 0.0:

(Real seconds: 0.0)

So you can call it with an argument like normal:

// wait 1.5 seconds
_wait(1.5)

Or you can omit the argument and let it use the default argument:

// Same as _wait() or _wait(0.0)
_wait

Any omitted trailing arguments will try to use default arguments to fill the gaps.

Let’s say there is a hypothetical method2() that has two default arguments:

( Object param1: "one"
  Object param2: "two
)
// Call it with 2 arguments as normal
method2(arg1 arg2)
 
// Omit the second (also the last) argument
// param2 will default to "two"
method2(arg1)
 
// Omit the first and second arguments
// param1 will default to "one"
// param2 will default to "two"
method2

SkookumScript also allows you to skip default arguments. You simply use a comma , to mark where an argument would have been and then follow with any arguments that you do want. If there is an argument prior to a skipped argument then the prior argument must be followed by a comma too.

// Omit the first argument and specify the second
// param1 will default to "one"
method2(, arg2)
 
// So essentially
method2(/*where arg1 would be*/, arg2)

So for further examples, assume hypothetical methods method0(), method1(), method2() and method3() with 0, 1, 2 and 3 default arguments respectively. Here are a bunch of the different ways you could call them:

method0()                  // optional ()
method0
 
method1(arg1)
method1()                  // default arg1, opt ()
method1                    // default arg1
 
method2(arg1, arg2)        // optional ,
method2(arg1 arg2)         // removed optional ,
method2(, arg2)            // default arg1
method2(arg1)              // default arg2
method2()                  // default arg1 & arg2, opt ()
method2                    // default arg1 & arg2
 
method3(arg1, arg2, arg3)  // optional ,
method3(arg1 arg2 arg3)    // removed optional ,
method3(, arg2, arg3)      // default arg1
method3(, arg2 arg3)       // default arg1 & removed opt ,
method3(arg1, , arg3)      // default arg2
method3(arg1, arg2)        // default arg3
method3(arg1 arg2)         // default arg3 & removed opt ,
method3(, , arg3)          // default arg1 arg2
method3(, arg2)            // default arg1 arg3
method3(arg1)              // default arg2 arg3
method3()                  // default arg1, arg2 & arg3, opt()
method3                    // default arg1, arg2 & arg3

Named arguments

You can also specify arguments by the parameter name you want to match them to during an invocation using named arguments. These can be especially handy with invocations that have many default arguments and you want to use most defaults except for one or two arguments at the end (far right). In most languages you would be required to specify all the arguments before the ones that you want to specify. In SkookumScript, simply specify the name of the parameter to match, follow it with a colon and then the argument expression. This looks very similar to variable rebinding. Once a named argument is used, any arguments that follow must also be named arguments.

Imagine a hypothetical method5():

( Object param1: "one"
  Object param2: "two"
  Object param3: "three"
  Object param4: "four"
  Object param5: "five"
)

Here is how you could call it with all the first four parameters using defaults and you want param5 to be different than the default:

// Using a named argument for param5
method5(param5:"Fifth")
 
// Using argument skipping (less readable)
method5(, , , , "Fifth")

NOTE Any named arguments are evaluated in their matching parameter order and not necessarily the order that they appear in the call.

method5(
  param1:"First"
  param2:"Second"
  param3:"Third"
  param4:"Fourth"
  param5:"Fifth")
 
// Evaluates in the order:
"First"
"Second"
"Third"
"Fourth"
"Fifth"
 
method5(
  param4:"Fourth"
  param5:"Fifth"
  param3:"Third"
  param1:"First"
  param2:"Second"
 
// Evaluates in the order:
"First"
"Second"
"Third"
"Fourth"
"Fifth"

Return arguments

All invocations are expressions and all expressions immediately return a result. The overall single result object returned by an invocation is called the primary result. Sometimes it can be useful to have more than one object returned by an invocation and one of the mechanisms that SkookumScript provides for this is return arguments. Return arguments follow any initial send arguments and a semicolon ;.

!return_arg1
!return_arg2
method(send_arg1 send_arg2; return_arg1 return_arg2)

Send arguments pass a reference to an object into a routine or closure and return arguments pass back a reference to an object and bind it to a variable or data member. The binding of a return argument is only done after an invocation has completed, which could be after multiple update frames for a coroutine.

Any object that a return argument variable was bound to before the invocation has completed will be replaced with a new binding after the invocation has completed.

If the first time a variable is used is with a return argument, its create temporary expression can be made right in the location of the return argument. Just add an exclamation mark—it’s a nice shorthand.

method(send_arg1 send_arg2; !return_arg1 !return_arg2)

If return arguments did not exist you could use send arguments to get results back though it is much more convoluted. You must first create some objects to pass into a routine by reference, then in the called routine these objects will have modifying routines called on them or they will be modified using an assignment, then after the called routine completes you can use them.

For example the Actor@eyes_view_point() routine is written with send parameters:

Returned send arguments
1
2
3
4
5
6
7
8
9
10
11
// Actor@eyes_view_point() using send parameters
(Vector3 out_location, RotationAngles out_rotation)

// Here is how it would be called and used:
// Make some default location and rotation objects
!loc: Vector3!
!rot: RotationAngles!
// Call routine that modifies the objects
actor.eyes_view_point(loc rot)
// Use them
do_stuff(loc rot)

It would be much cleaner if written with return arguments:

Return arguments
1
2
3
4
5
6
7
8
9
// Actor@eyes_view_point() using return parameters
(; Vector3 out_location, RotationAngles out_rotation)

// Here is how it would be called and used:
// Call routine that creates the objects
// and binds to loc and rot
actor.eyes_view_point(!loc !rot)
// Use them
do_stuff(loc rot)

Here are some working examples of return arguments:

!found?: "Hey, how is it going?".find?("how"; !idx)
println("found?:" found? " idx:" idx)

Prints out:

found?:true idx:5
nil

And this:

!names: {"Arthur" "Ford" "Zaphod" "Trillian"}
!name: names.any(; !idx)
println("name:" name " idx:" idx)

Called a few times randomly prints out:

name:Zaphod idx:2
nil
name:Trillian idx:3
nil
name:Ford idx:1
nil
name:Trillian idx:3
nil
name:Arthur idx:0
nil

Closures as arguments

Closures can be passed as arguments just like any other object.

// Loop this code 5 times
5.do((Integer idx)[println(idx)])

If the desired type of a closure is known from the surrounding context, then its parameters can be inferred. You still need to differentiate between a closure and a code block so if you omit the parameters, the closure needs to start with a closure receiver caret ^.

// Same as 5.do((Integer idx)[println(idx)])
5.do(^[println(idx)])
 
// (Integer idx) is inferred from the Integer@do() method.
// Take a look at the Integer@do() method in the SkookumIDE.

TIP Occasionally, it can be a bit mystifying to know where parameter names come from if parameters are inferred, though you can just go to the definition of a routine using Alt+G to see them. Many parameter names are the same across different routines (such as idx or item) and after a while you will get familiar with their use.

Inferring closure parameters saves a lot of typing and can simplify the code making it easier to read.

TIP Sometimes, especially with nested closures and loops, you may not want the parameters for closures to be inferred—see how to change the inferred item variable name in a do loop in this forum post.

Closure tail arguments

If a closure is the last parameter of the send parameter list (the tail) then the invocation brackets are not necessary. This mechanism for an invocation is called closure tail arguments. This helps remove the clutter of many brackets and makes code more readable.

// Same as 5.do((Integer idx)[println(idx)])
// or 5.do(^[println(idx)])
5.do[println(idx)]
 
// This can look a lot like native language primitives
5.do
  [
  println(idx)
  ]

If there are send arguments before the tail closure they are placed between the routine identifier name and the closure. The routine identifier acts as the beginning bracket and the closure acts as the ending bracket.

// The 2 is the first argument and the closure is the second argument
10.do_by 2 [println(idx)]

If closure tail argument invocation needs to use return arguments, a semicolon ; is placed after the closure and is followed by the return arguments.

!idx
!found?: {3 4 5}.find?[item.pow2?]; idx
println("found?: " found? ", idx: " idx)
// Prints:
// found?: true, idx: 1
 
found?: {3 5 7}.find?[item.pow2?]; idx
println("found?: " found? ", idx: " idx)
// Prints:
// found?: false, idx: 3

TIP You can build many of your own routines that look like native language primitives using closure tail arguments. To get ideas, you can see a few example routines that use closures as their last argument by using the Skookum IDE Members widget and search for routines that have “do” in their name. You can examine matching routines to see how they are written and most of them have example code of how they are called in their comments.

Closure tail arguments are nifty, clean and powerful and they are a language construct fairly unique to SkookumScript.

As a bit of trivia, the Swift programming language has a similar feature called trailing closures. SkookumScript originally had it implemented on November 21, 2013 and Swift was publicly released on June 2, 2014 so it must be an example of convergent evolution in language design or great mad computer scientists think alike.

The Ruby language has anonymous blocks and block arguments that also work similarly.

Binary and unary operator calls

Operators are constructs that act like routines though they differ a bit in their form and sometimes their semantics. All operators are syntactic sugar for regular method call equivalents.

There are three forms of operators:

  • infix binary operators: expr1 op expr2 Where expr1 is the receiver object the operator method is applied to and expr2 is the argument operand that is passed to the operator method.
  • prefix unary operators: op expr Where expr is the receiver object the operator method is applied to.
  • postfix unary operators: expr op Where expr is the receiver object the operator method is applied to.

Operand expressions

Both binary and prefix operators take as much of an operand expression following the operator as possible. In other words, even if a smaller part of an expression could stand on its own as a single expression, the parser will instead make a larger single compound expression if there is more code that can be part of the expression. If you want to group operators and expressions in some particular way use code blocks [ ] to group expressions and specify order.

For example:

expr1 op1 expr2.method op2 expr3
// The whole expression for the op1 operand is
expr2.method op2 expr3
// or in method form
expr1.op1(expr2.method().op2(expr3))
// Not just this part expression
expr2.method
// Or this part expression
expr2

There are four major categories of operators:

  • assignment (:=)
  • math (+, +=, -, -=, *, *=, /, /=, prefix -, postfix ++, postfix --)
  • comparison (=, ~=, >, >=, <, <=)
  • Boolean (and, or, xor, nand, nor, nxor, prefix not)

Assignment operator :=

Assignment is a binary operator := used to copy the logical value from the object on the right hand side to the logical value of the object on the left hand side.

How this is precisely done depends on the class type of the object and how its assignment method is written. Assignment methods typically return the receiver object so that they may be chained together (also called stringization).

!num:   42
!value: 123
// Assign 123 to the object that num refers to
num := value  // Now num and value are both 123
// Chaining - assign 1357 to both num and value
num := value := 1357

The assignment operator is syntactic sugar for the assign() method.

!num:   42
!value: 123
// Assign 123 to the object that num refers to
num.assign(value)  // Now num and value are both 123
// Chaining - assign 1357 to both num and value
num.assign(value.assign(1357))

See the assign() method in classes that allow assignment to learn any specific information for a class type.

SkookumScript uses := for assignment like the languages Smalltalk, Pascal, ML, ALGOL, Simula, Eiffel and others rather than = like the C family of languages and others. For a more in-depth look on this topic, see the Wikipedia articles Assignment versus equality and Confusion with assignment operators.

When to use assignment := or binding :

In some ways, assignment := and binding : are very similar and when to use one or the other can be confusing.

  • Assignment :=
    • copies the contents of an object to a different object usually of the same class type.
    • The object currently referenced by the identifier being assigned to must be already constructed and compatible for the assignment.
    • Multiple identifiers can reference the same object and if its contents are changed with an assignment then all the identifiers that reference that same object will be affected.
  • Binding :
    • changes the object reference (the address or pointer in memory) the identifier refers to which may be a different class type.
    • The old object previously referenced by the identifier being bound is unaffected (unless this was the last reference for the old object in which case it will be destructed and freed–we will go into more detail on this later).
    • Other identifiers that continue to reference the same old object before a bind are not affected—they just keep on referencing the old object.

Big objects

Big complex objects such as characters (NPCs) in a game usually cannot copy all their contents (name, position, hit points, animation, etc.) to a completely different character so they tend to only ever use binding to copy a reference from one identifier to another.

Big object assign vs. bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
!bob:    GoodGuy@'Bob'
!target: BadGuy@'Bad Bart'

// Has player glare at target (Bad Bart)
player._glare_at(target)

// Copy info from Bob and store in Bad Bart
// Depending on game engine integration
// probably will not work and be an error
target := bob

// Have target reference Bob
target: bob

// Has player glare at target (Bob)
player._glare_at(target)

Small/value objects

Setting a variable to a small object such as an Integer object often could use either an assignment or a bind. Both are fairly quick and simple.

Small/value object assign vs. bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
!jump_count: 1 // obj1

// Has player jump jump_count (1) times
jump_count.do[player._jump_up]

jump_count := 2 // obj2
// Copies 2 value from obj2 into obj1 and discards obj2

// Has player jump jump_count (2) times
jump_count.do[player._jump_up]

jump_count: 42 // obj3
// Rebinds jump_count to obj3 (42) and discards obj1

// Has player jump jump_count (42) times
jump_count.do[player._jump_up]

So in this case, both the assignment jump_count := 2 and the rebind jump_count: 42 achieved the same end goal—the value referred to by jump_count changed.

This only really becomes interesting when the same object reference is shared by multiple identifiers.

Shared objects and assign vs. bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
!jump_height: 100.0 // obj1
!low:          50.0 // obj2
!high:        200.0 // obj3

// Has player jump by jump_height (obj1: 100.0)
player._jump_up(jump_height)

// Copy info from `low` (obj2: 50.0)
// and store in `jump_height` (obj1)
jump_height := low
// jump_height still bound to obj1
// which is overwritten with 50.0
// obj2 is also still 50.0

// Has player jump by jump_height (obj1: 50.0)
player._jump_up(jump_height)

// Change jump_height (obj1) contents to 75.0
jump_height := 75.0
// low (obj2) is still 50.0

// Has player jump by jump_height (obj1: 75.0)
player._jump_up(jump_height)

// Rebind `jump_height` (was bound to obj1)
// to object bound to `high` (obj3: 200.0)
jump_height: high
// obj1 is no longer referenced
// `jump_height` is now was bound to obj3: 200.0

// Has player jump by jump_height (obj3: 200.0)
player._jump_up(jump_height)

// Change high (obj3) contents to 150.0
high := 150.0
// jump_height (obj3) is bound to the same object and also 150.0

// Has player jump by jump_height (obj3: 150.0)
player._jump_up(jump_height)

So changing an object that is referenced by multiple identifiers will change them all. Often this is a desired behavior, though sometimes it is unexpected and not desired.

Math operators

You will find standard math operators in classes such as Integer and Real, though they can be in any class.

Operator Method Example Op Example Method
+ add() num + 2 num.add(2)
+= add_assign() num += 10 num.add_assign(10)
- subtract() num - 2 num.subtract(2)
-= subtract_assign() num -= 10 num.subtract_assign(2)
* multiply() num * 2 num.multiply(2)
*= multiply_assign() num *= 2 num.multiply_assign(2)
/ divide() num / 2 num.divide(2)
/= divide_assign() num /= 2 num.divide_assign(2)
- negated() -num num.negated()
++ increment() num++ num.increment()
-- decrement() num-- num.decrement()

NOTE There is no prefix version of increment ++num or decrement --num so the postfix versions of these operators typically, such as with the Integer and Real classes, make any changes (increment/decrement) and then return the changed object.

NOTE Like the Smalltalk language, math operators do not have a precedence order in SkookumScript. Use code blocks [ ] to group expressions and specify order.

// Will be parsed as 2 * [3 + [4 * 5]]
2 * 3 + 4 * 5  // = 46
// in method from:
2.multiply(3.add(4.multiply(5)))
// so use square brackets
[2 * 3] + [4 * 5]  // = 26
Subtract operator vs. negative number or negated

Since SkookumScript does not need any sort of delimiter between expressions, it is possible to confuse these two possible uses of the minus - symbol:

  • a single expression comprised of one expression subtracting a second expression expr1 - expr2
  • two separate expressions with the second expression being a negative number -42 or an expression using the negated prefix operator -expr

The SkookumScript syntax follows the rule-of-thumb that most humans would use visually:

In order to be recognized as single subtract - operator expression and not an expression followed by a second expression starting with a minus sign, the minus symbol - must either have whitespace following it or no whitespace on either side.

So for example:

// Considering numbers
1-2    // one subtract two: 1 expression
1- 2   // one subtract two: 1 expression
1 -2   // *one and negative two: 2 expressions
1 - 2  // one subtract two: 1 expression

// Considering expressions
expr1-expr2    // expr1 subtract expr2: 1 expression
expr1- expr2   // expr1 subtract expr2: 1 expression
expr1 -expr2   // expr1 and negated expr2 : 2 expressions
expr1 - expr2  // expr1 subtract expr2: 1 expression

Comparison operators

The comparison (or relational) operators are used to determine logical or structural equality and order and they all return true or false. They are used in classes such as Integer, Real, String and Symbol though they can be in any class.

Operator Method Example Op Example Method
= equal?() num = 42 num.equal?(42)
~= not_equal?() num ~= 42 num.not_equal?(42)
> greater?() num > 42 num.greater?(42)
>= greater_or_equal?() num >= 42 num.greater_or_equal?(42)
< less?() num < 42 num.less?(42)
<= less_or_equal?() num <= 42 num.less_or_equal?(42)

SkookumScript uses = to test for logical equality which is the same as the international mathematical standard, Smalltalk, Pascal, ML, ALGOL, Simula, Eiffel and others rather than == like the C family of languages and others. For a more in-depth look on this topic, see the Wikipedia articles Assignment versus equality and Confusion with assignment operators.

Likewise, ~= is used for not equal which is same as Smalltalk, Lua, and MATLAB rather than !=. This is partly because ! in SkookumScript generally means “create” rather than “not”.

TIP To compare physical equality (whether two expressions refer to the same object—in other words their memory addresses are identical) the same?() method is used. For example: obj1.same?(obj2)

For comparison in other languages, testing if references are identical/equivalent in C++ uses &obj1 == &obj2, JavaScript and Swift use obj1 === obj2, C# uses object.RefrenceEquals(obj1, obj2), Python uses obj1 is obj2 and Ruby uses obj1.equal?(obj2).

Boolean operators

You will find standard Boolean (or logical) operators in the Boolean class, though they can be in any class.

Operator Method Example Op Example Method
and and() test1? and test2? test1?.and(test2?)
or or() test1? or test2? test1?.or(test2?)
xor xor() test1? xor test2? test1?.xor(test2?)
nand nand() test1? nand test2? test1?.nand(test2?)
nor nor() test1? nor test2? test1?.nor(test2?)
nxor nxor() test1? nxor test2? test1?.nxor(test2?)
not not() not test? test?.not

TIP Be particularly attentive with the not operator when grouping operand expressions—they take as big of an expression as possible and you may want to specify what part of the expression it affects. If it would be easier to use it as a postfix method, just add .not to an expression.

Words are used for Boolean operations rather than symbols such as &/&& or |/|| since their meaning is obvious, they are so few characters and they get syntax highlighting in the SkookumIDE to help them stand out.

It may also surprise some to learn that C++ and other C family languages also allow the word versions for Boolean logical operators of and, or, xor and not to be used interchangeably with &&, ||, ^ and !.

TIP Bitwise manipulation in SkookumScript is done with methods rather than symbol based operators. Several such methods starting with bit_ can be found in the Integer class: bit_and(), bit_not(), bit_or(), bit_set?(), bit_shift_down(), bit_shift_up() and bit_xor(). Since SkookumScript is a high-level language, bitwise operations should not be frequently used so bitwise symbol based operators have been omitted by design.

Short-circuit evaluation

Short-circuit evaluation (or minimal evaluation) is when the second operand of a binary operator is evaluated only if the first operand is not sufficient to determine the value of the expression. For example, if the first operand of the Boolean class and evaluates to false then the overall result will be false and there is no need to evaluate the second operand. Because of this conditional or lazy evaluation, short-circuit operators are, in effect, control structures rather than simple operator calls.

Short-circuit evaluation is often used as a gate to skip or avoid undesired side effects of a following test.

For example, using the Boolean and operator which is short-circuiting:

if guy.alive? and guy.sees_player?
  [
  guy.jump
  ]

So guy.sees_player? won’t even be called if guy.alive? returns false.

NOTE Only the and, or, nand and nor operators in the Boolean class (and possibly any subclasses) use short-circuit evaluation. If these operators are used on other classes, they will not use short-circuit evaluation.

Boolean short-circuit operators are:

Operator Skip op2 if op1 Result if shorted
and false false
or true true
nand false true
nor true false

Instantiation (creating new objects)

Object instantiation creates an instance object for a particular class type—in other words, it is used to make new objects.

In order to understand instantiation calls, we will need to know a little bit about instance constructor methods. Constructor methods are used to initialize an object and there are two main types: default constructors with a single exclamation mark (!) or named constructors which start with an exclamation mark followed with an identifier name starting with a lowercase letter (!copy, !xyz, !new, !null).

TIP In SkookumScript, the exclamation mark ! is often used to mark when new things are created.

An instantiation call is made by specifying the name of the class of object to create and then the name of the constructor to call to initialize the object.

Example Instantiations
1
2
3
4
5
6
7
// Default constructors
!mind:   Mind!
!vector: Vector3!

// Named constructors
!vec1: Vector3!xyz(1 2 3)
!vec2: Vecotr3!copy(vec1)

In addition to the default constructor !, the copy constructor !copy is a very useful and common constructor. It makes a new copy of an object from another object of the same type.

Objects created via instantiation in SkookumScript are generally created on the memory heap (though in reality often through reused memory pools, shared engine structures and other mechanisms used for memory and speed efficiency) so instantiation is somewhat analogous to the new operator in C++.

Literals automatically create an object instance as well as giving it an initial value. There are only 7 literal class types and thousands of classes that don’t have a literal syntax, so object instantiation is one of the key mechanisms used to create all the other types of class objects.

The simple class types that have literals also have default constructors, though it usually looks more aesthetically pleasing to use a literal to create them rather than to use instantiation.

Instantiations for classes with literals
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
!int1: 0
!int2: Integer!

!real1: 0.0
!real2: Real!

!bool1: false
!bool2: Boolean!

!str1: ""
!str2: String!

!str1: ''
!str2: Symbol!

!str1: {}
!str2: List!

Expression Instantiation

Expression instantiation is a form of syntactic sugar to shorten an object instantiation. Instead of using a class name to do the instantiation, an expression is used both to determine the class of the object to create and to be used as the first argument.

To make a copy of a String object you could use the String copy constructor:

!str1: "hello"
!str2: String!copy(str1) // str1 copied as new str2

Using an expression instantiation this can be written more concisely:

!str1: "hello"
!str2: str1!copy // str1 copied as new str2

This can be used with any constructor that has its first argument as the same type of class as the class being constructed.

Since copy constructors are used so frequently, if the copy part of !copy is omitted and just ! is used, the parser assumes the full !copy. So the copy can be written even more concisely:

!str1: "hello"
!str2: str1! // str1 copied as new str2

So all of these are equivalent:

!str1: "hello"
// Make new copies of str1
!str2a: str1!
!str2b: str1!copy
!str2c: String!copy(str1)

So whenever you see an exclamation mark after an expression, you know that it is making a new copy of the result object by calling the copy constructor.

This is an easy mechanism to ensure that you pass a copy of an object to a method rather than just passing the reference of an object. This essentially mimics argument passing by value.

some_method(obj_to_not_pass_as_ref!)

It is also handy to allow a routine to accept a simple object reference and then to make a copy of the object in the body of the routine if the routine needs to modify it or if it needs to hold onto the object across frames such as with coroutines.

Making a copy in a routine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Definition for shout3() closure
!shout3: (String message)
  [
  // Make a copy of the parameter before modifying it
  !message_copy: message!
  message_copy.uppercase
  // Print out uppercase version of message three times
  3.do[println(message_copy "!")]
  ]

!str: "don't panic"
// Send str as a reference
shout3(str)

// str unchanged
println(str)
Modified copies

Often you’ll want to make a different version of an existing object and you want to keep the original object unmodified. One way you can do this is to first make a copy of the original object and then to call a modifying method on the new copy.

!str1: "hello"
// Make a new string that is an uppercase version of str1
!str2a: String!copy(str1).uppercase

// Using an expression instance
!str2b: str1!copy.uppercase

// Using an expression instance with the default copy
!str2c: str1!.uppercase

Expression instances have one more shortcut to offer—they can also figure out if a particular named constructor exists or if it can break the !name apart and use the copy constructor !copy() and a separate method name(). So you can also do this:

!str1: "hello"
// Make a new string that is an uppercase version of str1
!str2c: str1!uppercase

This is how the parser figures out the expression instantiation str1!uppercase:

  • First the parser determines the result object class type of the expression str1. In this case it is String.
  • Next the parser looks for a String!uppercase() constructor. It doesn’t have such a constructor.
    • If it were found, it would call String!uppercase(str) and be done.
  • Next it determines if it has a copy constructor !copy(). It does.
    • And it determines if it has a method called uppercase(). It does.
    • If both are found, it calls String!copy(str).uppercase()

So all of these are equivalent:

!str1: "hello"
// Make a new string that is an uppercase version of str1
!str2a: str1!uppercase
!str2b: str1!.uppercase
!str2c: str1!copy.uppercase
!str2d: String!copy(str1).uppercase

str1!uppercase is more concise than String!copy(str1).uppercase. And if an !uppercase() constructor is added in the future (which would probably be more efficient than two calls), then it will use that instead: String!uppercase(str)

Whenever you see some expression and !name you now know that it means make a new copy and modify it. For this reason, many of the methods in objects are modifying methods since you can always use ! + modding_method to make a non-modifying version.

Making a modified copy in a routine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Definition for shout3() closure
!shout: (String message)
  [
  // Make a modified copy of the parameter
  !message_copy: message!uppercase
  // Print out uppercase version of message three times
  3.do[println(message_copy "!")]
  ]

!str: "don't panic"
// Send str as a reference
shout3(str)

// str unchanged
println(str)

Index operator {idx}

The index operator { } is generally used for indexing into and retrieving a sub-part of an object, especially List objects, though it can be made available to any class.

List objects use the index operator to retrieve an item object from a specified 0-based index position.

For example: list{idx}

List index operator {}
1
2
3
4
!list: {0 "one" 2.0 'three'}
println(list{0})  // 0
println(list{1})  // "one"
println(list{3})  // 'three'

The index operator { } is just syntactic sugar for the at() method. Any class that has at() such as List can also use the index operator.

List index method at()
1
2
3
!list: {0 "one" 2.0 'three'}
// So this method call is the same as list{1}
list.at(1)

See the at() method in classes that have it to learn any specific information for a class type.

Index set operator {idx}:

The index set operator { }: is very similar to the index operator though it sets a sub-part of an object. It also can be made available to any class.

List objects use the index set operator to set an item object reference at the specified 0-based index position to the specified object reference.

For example: list{idx}: obj

List index set operator {}:
1
2
3
4
5
!list: {0 "one" 2.0 'three'}
// Replace the old object item reference with a new one
list{1}: "ONE"    // {0 "ONE" 2.0 'three'}
list{0}: nil      // {nil "ONE" 2.0 'three'}
list{3}: 'Three'  // {nil "ONE" 2.0 'Three'}

Similar to a variable, the index set operator essentially rebinds the item slot in the list to the object reference.

The index set operator { } is just syntactic sugar for the at_set() method. Any class that has at_set() such as List can also use the index set operator.

List index set method at_set()
1
2
3
4
!list: {0 "one" 2.0 'three'}
list.at_set(1 "ONE")    // {0 "ONE" 2.0 'three'}
list.at_set(0 nil)      // {nil "ONE" 2.0 'three'}
list.at_set(3 'Three')  // {nil "ONE" 2.0 'Three'}

See the at_set() method in classes that have it to learn any specific information for a class type.

TIP To assign an object in-place and not replace (or rebind) it with a new object, use the index operator and an assignment (or other modifying methods) on the resulting object.

List item assignment
1
2
3
4
5
!list: {0 "one" 2.0 'three'}
// Keep the same object and change its content
list{1} := "ONE"    // {0 "ONE" 2.0 'three'}
list{0} := 42       // {42 "ONE" 2.0 'three'}
list{3} := 'Three'  // {42 "ONE" 2.0 'Three'}

Apply operator %

An apply operator % calls a routine on zero, one or more objects (in the case of a list) and comes in two varieties based on whether it is used on a list or a non-list.

List apply

If the expression that the apply operator is called on is a List object, then the routine call that follows the % is called on each item in the list respectively and once completed the list itself is returned. Any arguments are evaluated for each call. If the list is empty then the routine is not called and any arguments are not evaluated.

{1 -2 3 -4}%negate
// returns {-1 2 -3 4}

The apply operator works differently for methods than coroutines. If a method is applied then it is called and completed with each item in succession and then the list object is returned as the result.

If a coroutine is applied then it is called on each item concurrently (all starting in the same update frame) and the next expression is run only after all the coroutines for each item in the list have completed.

Example apply coroutine in SkookumDemo
1
2
3
4
5
6
// Get a list of all the Enemy robots and have
// them all simultaneously path to the player.
Enemy.instances%_path_to_actor(player_pawn)
// Once the *last* robot has pathed to the player
// then have an explosion around the player.
player_pawn._boom

There is another variant of the apply operator called the apply race operator %> which calls the coroutine concurrently like the apply operator though it aborts any coroutines that have not yet completed as soon as the first coroutine completes—i.e. whichever item finishes its coroutine first. So they all run a race and the fastest wins.

Example apply race coroutine in SkookumDemo
1
2
3
4
5
6
// Get a list of all the Enemy robots and have
// them all simultaneously path to the player.
Enemy.instances%>_path_to_actor(player_pawn)
// Once the *first* robot has pathed to the player
// then have an explosion around the player.
player_pawn._boom

Non-list apply

If the expression that the apply operator is called on is not a List object, then if it is non-nil the routine call that follows the % is called on the object and if the it is a nil object then the routine is not called, any arguments are not evaluated and nil is returned. So again:

  • non-nil: call is made just as if % were a .
  • nil: call is ignored and nil is returned
// Assume there is a pawn named 'RoboChar1'
// RoboChar1 goes boom!
Enemy.find_named('RoboChar1')%_boom
// _boom() runs

// Assume there is no pawn named 'Nobody'
// so nil is returned by find_named()
// and _boom is not called
Enemy.find_named('Nobody')%_boom
// _boom() ignored

TIP In a way, a non-list is treated either like a one item list (non-nil) or like an empty list (nil).

The apply operator % on a non-list is similar to calling methods through optional chaining ?. in Swift.

Closure invoke operator

A closure invoke operator () (called a call operator in C++ and some other languages) is used to invoke or call a closure. It looks just like calling any other routine except it is used on an expression that evaluates to a closure object.

Closure invoke operator example
1
2
3
4
5
!triple: (Integer num)[num * 3]

// Use closure invoke operator () on closure triple
triple(4)
// returns 12

NOTE Any captured variables in a closure object have their references bound when a closure literal is initially evaluated. If a variable has been rebound : to a new object by the time a closure object is invoked, then the captured variables will still be bound to whatever objects they were bound to during the initial evaluation of the closure literal.

Closure with capture invoke
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
!value:  3
// Capture `value` variable
!multiply: (Integer num)[num * value]

println(multiply(4))
// 12

// Modify existing `value` object:
// Assign value to -2
value := -2

// multiply still references the same object as `value`
println(multiply(4))
// -8

// Rebind `value` to new 10 object:
value: 10

// multiply still references the old `value` (-2) object
// and not new `value` (10) object
multiply(4)
// returns -8 (not 40)

Once a closure object is invoked, it may modify objects referred to by any captured variables. The captured objects and any changes made on them will be kept stored in the closure object.

Closure invoke modifying a capture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
!value:  0
// Capture `value` variable
!by_twos:
  ()
    [
    // increment captured value
    value += 2
    ]

println("result:" by_twos() " value:" value)
// result:2 value:2
println("result:" by_twos() " value:" value)
// result:4 value:4

// Modify existing `value` object:
// Assign value to -10
value := -10

println("result:" by_twos() " value:" value)
// result:-8 value:-8
println("result:" by_twos() " value:" value)
// result:-6 value:-6

// Rebind `value` to new 10 object:
value: 10

// value (-6) in multiply_increment now disconnected
// from value (10) in outer scope
println("result:" by_twos() " value:" value)
// result:-4 value:10
println("result:" by_twos() " value:" value)
// result:-2 value:10

SkookumScript does not have a invoke operator for functional objects since closures are much simpler to create and more powerful.

Conversion calls

Conversion methods describe the steps needed to convert one type of an object to another. A conversion method is located in the class type that is being converted from (conversion source) and it has the same name as the class that it is converting to (conversion target).

Calling a conversion method
1
2
3
4
5
!str42:  "42"
!int42:  str42.Integer // Convert String to Integer
!real42: str42.Real    // Convert String to Real
!name42: str42.Name    // Convert String to Name
!sym42:  str42.Symbol  // Convert String to Symbol

Each conversion source class may only have one conversion method for a given conversion target class.

TIP Conversion calls may not pass any arguments. (Though they may in the future.) If a more complex conversion is needed, then either:

  • create a normal method on the source class with as many arguments as needed that returns the target class
  • create an appropriately named constructor method in the target class and it can have as many arguments as needed and then you instantiate the target class using the constructor with the source object as one of the arguments.

NOTE Conversion methods are used by class conversion primitives obj>>ClassType. Class conversion calls can be used in the place of class conversion primitives, though unlike class conversion primitives they must always explicitly specify the conversion target class name.

Type Primitives

SkookumScript’s type system is powerful, flexible, safe, and gets out of the way as much as possible. Unlike many other scripting languages, SkookumScript is fully statically type-checked at compile-time, which catches many bugs before a game runs.

Static types are like a quantum superposition of all possible execution paths. So static types are the truest form of live code!

From time to time, you will need to convert one type of an object to another using a class conversion and sometimes you will need to help the SkookumScript parser out and tell it what type an object really is with a class cast or two.

Class Conversion >>

A class conversion operator >> is used to take an existing object of one class type and to construct a new object of a different class type.

To convert an object, place two greater than symbols >> after the object expression and then specify the name of the class that you want to convert to.

Class conversion examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Original types
!str:    "123"   // String
!answer: 42      // Integer
!pi:     3.1416  // Real

// Converted types
!str2int:    str>>Integer
!str2real:   str>>Real
!str2symbol: str>>Symbol
!str2name:   str>>Name

!int2real: answer>>Real
!int2str:  answer>>String

// Sometimes a conversion isn't perfect:
// A 3.1416 Real converts to just a 3 Integer
!real2int: pi>>Integer
!real2str: pi>>String

Remember Conversion Calls? They are the methods that class conversions call to do the actual work. Whenever a class conversion is needed, the appropriate class conversion call is used internally. If the desired conversion call doesn’t exist, then there is no conversion path and a class conversion to that class will give a parser error.

TIP Every class has a String() conversion method, so any class can be converted to a string >>String. The default String() conversion method just returns the class name of an object though many classes have their own custom String() conversion methods.

Redundant conversion

If you try to convert an object to the type that it already is and the parser knows this, it won’t let you do it since it is redundant.

!str: "123"
str>>String

This gives the error:

The expression being converted is already known to be an instance of the class String so converting it to an instance of the class String is redundant.

If at compile time the parser doesn’t know that the type of the object will already be the type that you are converting to, it will allow the class conversion. During runtime if the object is already the type being converted to, then the class conversion will do nothing.

Inferred conversion class

If the parser knows the desired type for an expression, such as an argument being passed to a routine, then the class conversion can have its class type inferred. All that is needed is the >> and the class name can be omitted.

Conversion with inferred class
1
2
3
4
5
6
7
!name_str: "Marvin"

// Assume there is a use_named(Named name) method
use_named(name_str>>)

// Just the same as if this were used:
use_named(name_str>>Name)

By design, coercion (implicit conversion) is not allowed. You must at least explicitly use >> to indicate that a conversion to the expected type is intentional.

No implicit conversion
1
2
3
4
!name_str: "Marvin"

// Coercion (implicit conversion) is not allowed
use_named(name_str)

This gives the error:

The argument supplied to parameter named name was expected to be of type Name and it is type String which is not compatible.
However, String has a Name() conversion method so it can easily be converted with the >> class conversion operator.

Conversion variations

There are several ways to convert an object to the type you want:

  • class conversion obj>>String
    • can infer the type obj>>
    • does no action if already the target type
  • conversion call obj.String
    • always called
    • very intuitive and looks nice
  • arbitrary custom method obj.as_string
    • can have any number of arguments
    • can have several differently named variants
    • less obvious to discover, though as_ recommended as a prefix
  • constructor in target type String!from_my_type(obj)
    • can have any number of arguments
    • can have several differently named constructors: TargetType!from_my_type1(), TargetType!from_my_type2()
    • doesn’t look as nice as class conversion or conversion call
Conversions in all variations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
!name_str: "Startibartfast"

// When parser does not know desired type:
!str2name1: name_str>>Name
!str2name2: name_str.Name
!str2name3: Name!(name_str)

// There could also be some method such as `as_name()`
// that does conversion though it won't be as obvious
// for a user to discover it.
!str2name4a: name_str.as_name
// Could also have arguments and return arguments
!str2name4b: name_str.as_name(arg1 arg2; !rarg1 !rarg2)

// When parser knows desired type:
// Assume there is a use_named(Named name) method
// Inferred Name class
use_named(name_str>>)
// Not inferring Name class
use_named(name_str>>Name)
use_named(name_str.Name)
use_named(Name!(name_str))
use_named(name_str.as_name)

Class Cast <>

A class cast operator <> is a hint to the parser to use a particular class type for an expression. It is called a hint since the object is only being labelled differently which may allow the parser to use it in an otherwise different manner, the object itself is not changed. At runtime all objects know their type, though when code is parsed (at compile-time) the parser may not know what specific type an object will be.

To cast an object, place a less than followed with a greater than symbol <> after an expression and then specify the name of the class that the parser should assume the resulting object will be.

expression<>DesiredClass

The SkookumScript cast operator <> is similar to the :?> operator in F#, a combination of the static_cast<>() and dynamic_cast<>() in C++, the as! operator in Swift or the as in Rust.

If code can be written in such a way that no casting is needed, then that is should probably be preferred over code that requires casting.

When you do need to cast a type, you should only do so when you know it to be safe. Either you know this based on the flow of the code logic or various tests are make to ensure that a cast will be valid. If the type is not what you say it will be, during runtime an error will be raised to alert you of the discrepancy.

Though casts are considered just a parser hint, they will still do a sanity typecheck during runtime in debug builds and raise an error if the class type isn’t what is expected. Casts do nothing in non-debug builds.

So far in the Primer we haven’t talked much about object-oriented programming or the class hierarchy. In order to better understand class casting it helps to work with some example classes and their superclass and subclass relationships.

Class tree

Example class hierarchy tree with some classes from the SkookumDemo project. Arrows point from superclasses to subclasses. All classes have Object as their base superclass. We’ll refer to this class tree in the next sections.

There are essentially three categories of casting in SkookumScript:

  • downcasting from a superclass to a subclass
  • reduce casting from several potentially unrelated class types to one class type
  • illegal casts including equivalence casting to the same type of class, upcasting from a subclass to a superclass and unrelated casting where there is no direct path to change from one class to another

Downcasting

Downcasting (also called type refinement) is used to switch from a superclass down to one of its subclasses - such as Actor to Enemy (see the example class tree). Once casted, you may then use all the members of the subclass that were not available to the superclass.

1
2
3
4
5
6
7
8
// The HitResult class has an @actor data member and
// the parser only knows that it is an Actor type.
// You may want to cast to an Enemy subclass:
if hit_result.@actor.class_of(Enemy)
  [
  // _go_berzerk() is a coroutine in the Enemy class
  hit_result.@actor<>Enemy._go_berzerk
  ]

Reduce Casting

A reduce cast narrows from one of several possible types to a single type. SkookumScript has a special union class type where the parser knows that an expression is going to be one of several possible types, though it doesn’t know which one it will be until runtime.

For example, a list with several item class types has items that are a union of all their known classes.

1
2
3
4
5
6
7
8
// List with item type <Symbol|Integer|String|Real>
!list: {'zero' 1 "two" 3.0}

// Error since parser doesn't know which type it will be
//list{2}.uppercase

// Reduce possible types to just String
list{2}<>String.uppercase

In the code above, the parser knows that String is one of the possible classes in <Symbol|Integer|String|Real> so it assumes you know what you are doing and allows it.

Casting off nil/None

Expressions often will evaluate to either a specific class or the nil object. For example, @?'RoboChar1' or Actor.find_named("RoboChar1") will either return an actor named “RoboChar1” or nil if there is currently no such actor. The type is <Actor|None> where None is the class of the nil object.

If you are sure it is an Actor then you can cast it and access Actor members.

1
2
3
4
5
6
7
8
9
// type is <Actor|None>
// Could be robot (of class Actor) or nil
!robo1: @?'RoboChar1'

if robo1.not_nil?
  [
  // Reduce to just Actor
  println(robo1<>Actor.actor_location)
  ]
Combined Downcast and Reduce Casts

You can also do a combination of a downcast and a reduce cast.

1
2
3
4
5
6
7
8
9
// type is <Actor|None>
// Could be robot (of class Actor) or nil
!robo1: @?'RoboChar1'

if robo1.not_nil?
  [
  // Reduce to Actor and downcast it to Enemy
  println(robo1<>Enemy._go_berzerk)
  ]

Illegal Casts

If a cast seems redundant or incorrect, the parser will give an error saying that it is illegal.

An equivalence cast is redundant since the expression is already that class.

"hello"<>String

An up cast from a subclass to a superclass is redundant since the expression is already matching that superclass.

"hello"<>Object

An unrelated cast has no derivation path from the current class to the desired class.

For a unary (single) class, it is a unrelated cast if it is not a subclass.

"hello"<>Enemy

For a union class, it is a unrelated cast if none of its set of classes has a derivation path.

{'zero' 1 "two" 3.0}.any<>Enemy

None of the item classes above are an Enemy or a subclass of Enemy.

Inferred Casting

If the type of a cast can be figured out by the parser using the surrounding context, then the type can be omitted after the cast operator <> and the class to cast to will be inferred.

<SomeClass|None> will remove the None and reduce down to just SomeClass since it is the more interesting class and by definition you cannot do anything interesting with nil. This is similar to an apply operator % on a non-list object.

Passing an expression as an argument will infer the class type of the argument.

1
2
3
4
5
6
7
8
9
// Infer `Enemy` by getting rid of `None`
Enemy@?'RoboChar1'<>._go_berzerk
// Infer `Enemy` since that is the type desired by _chase()
Enemy@'RoboChar1'._chase(@?'RoboChar2'<>)

!robo1: Enemy@'RoboChar1'
!robo2: @?'RoboChar2'
if robo2.not_nil?
  [robo1._chase(robo2<>)]

Next up

Check out the rest of the documentation and move on to some tutorials.