The FFL Improvement Thread


#1

I recently had a discussion with Krista, that went something like this:

me: So, let me ask you something, let’s say you – you, yourself – had to develop a new game, and your family’s livelihood development depended on it … what language would you use to program all the game logic, all the object behavior in? If the game had to be right, and work reliably, and you develop it as quickly and conveniently as possible.
her: Well, there are some choices, FFL, or Python, or C++, or Lua…
me: Yes. And remember, you don’t want the “cleanest” or most “elegant”. You want the language that is going to get a game delivered that will put food on your table.
her: I’d use C++
me: Yeah. I’d use C++ too.
me: But why? There are these higher level, more elegant languages that are meant to be better for rapid development. Why would we use C++?

We spent some time discussing things, and our basic conclusion was that FFL is roughly in the same ‘bucket’ as Python and Lua, and similar for productivity. For us though – and do remember that Krista and I are seasoned developers – C++ is just more efficient. Why? Largely because it’s type safe, mature, and has well-established best practices.

It’s a little … disconcerting … to say the least, to spend lots of time developing a custom language that we ourselves find less productive than alternatives.

Now, a big advantage of FFL is that its learning curve is much lower than C++. So people who are not me or Krista can be much more productive in it than they would be in C++. But, it would be satisfying if FFL was actually as productive for its intended uses as C++ is, for me and Krista, and ideally more productive.

Thus, I want to use this thread to outline, analyze, and introspect on what makes C++ such a usable language for us, and what we can integrate into FFL to make it more usable, enjoyable, and productive – certainly for seasoned developers, and hopefully for everyone.

There are a number of features of C++ that we felt make it the most productive language for us. But the most important was static type checking. That is, in C++, we are very strict about putting in as much type information as possible, and then the compiler telling us about obvious mistakes.

It is also much easier to reason about types, to be able to tell what a function does on first glance, if it is explicit about what types it receives and returns. As such, I’ve started to introduce some optional type features to FFL.

Before you had something like,

	set_light_size: "def(lsize) [set(vars.lightSize, lsize), fire_event(self, 'light_size')]",

Now you can add type information like this:

	set_light_size: "def(int lsize) -> commands [set(vars.lightSize, lsize), fire_event(self, 'light_size')]",

This makes it explicit that the function accepts an int as a parameter, and returns a stream of commands for the game engine as a result.

The legal types we have are,

  • int
  • decimal
  • string
  • null
  • list
  • map
  • object
  • commands – a special type which represents a stream of commands such as events return to the game engine.
  • any – can be anything at all. The default if you don’t specify anything.

If you pass a function something that it’s not expecting, it’ll die, on the spot. Likewise, if a function returns something other than promised, it’ll also die.

This is good, but we are working toward better. Now that we pass along the types of parameters, our FFL parser tries to analyze code paths and work out at compile time, what type an expression is.

Just having vague types like ‘list’ doesn’t tell you much though. How do you know what kinds of elements the list contains? I’ve developed a syntax to allow for that. Here are some examples:

  • [int] – a list of integers
  • [[int]] – a list of lists of integers
  • {int -> string } – a map of ints to strings
  • { [int] -> {string -> string} } – a map of lists of integers to a map of strings to strings.

Thus, ‘list’ is really just a shorthand for [any] and ‘map’ for {any -> any}. It’s better to give more details where possible!

So, you can do things like,

def([int] mylist, {string -> string} mymap) -> int

to give exact details about what’s going in and what’s coming out.

We also support type unions. A type union is simply a value that may be one of multiple types. Type unions use a very simple syntax:

def(object|null obj) -> int|decimal

This defines a function taking an object or null and returns an integer or decimal.

Note that if a function accepts null it must explicitly say so. This is very intentional. If a function just says it takes an ‘object’ and you pass it null, you’ll get very loud complaints!

One other nicety is the addition of functionoverloads. Suppose you had a function that made objects talk:

talk: "def(object obj) ((some code here))

But suppose you often found yourself calling this on lists of objects. You want the function to be able to take a list of objects, and when invoked this way, do it for every object in the list. So, you do this:

talk: "def(object|[object] obj) if(is_list(obj), map(obj, talk(value)), ((some code here)))"

This works, but is somewhat cumbersome, in the type signature as well as the if logic. So, instead we can now create an overloaded function like this:

talk: "overload(
  def([object] obj) map(obj, talk(value)),
  def(object obj) ((some code here))
)"

When called, this function checks each function signature and uses the first function that matches the types of the arguments.

This is a starting point toward making FFL nicer to work with, and more robust. When working in C++, I can carefully bang out a program that works nice and reliably. I’ll make mistakes, of course, but I’ll fix them quickly and efficiently. With FFL it’s easier for mistakes to linger, for an occasional bizarre unexpected case to occur. We want to move toward eliminating this.

The next step – which I am working on next – is to make it so all the built-in objects that Anura has – things like custom objects, shader objects, variable storage objects, and so forth all have named types that are known to FFL.

For instance, we want to be able to make a function that looks like this:

def(frogatto_playable frogatto) -> commands frogatto.talk("hello")

A function like this should be able to be analyzed as soon as it’s parsed, and without executing it once, the engine should do all the following checks:

  • Is there a type that has been registered with FFL called ‘frogatto_playable’?
  • Does frogatto_playable have a property, ‘talk’?
  • Is this property, ‘talk’, a function which can be invoked with a single string as its argument?
  • Does this frogatto_playable.talk function, when invoked, return commands?

If the answer to any of these is ‘no’, then the engine will immediately complain. You won’t have to wait until the game gets into a situation where it is called.

Achieving this is still a little ways off, but I think once we get there i’ll greatly improve FFL’s productivity. I intend to add more posts as I go, adding more and more features to make FFL what I would consider an “industrial strength” language.


#2

Sneak preview of what I’m working on now:

[card.card_type.draw_id | card <- level.chars, card is obj card, card.in_hand, card.marked_as_keep]

What is special about this code? I’m working on making it so when the formula is parsed the engine works out that ‘card’ must be a card object. Thus, all the lookups such as card.in_hand and card.marked_as_keep are checked to make sure those members exist in card, and if any of them do not, it will throw an error complaining.

Note that this happens at formula parse time – the formula doesn’t have to actually be executed, so if it’s some obscure formula that hardly ever happens it’ll still complain about the error.

In addition, it’ll look up card.card_type.draw_id, see that it’s an integer, and realize that this expression must evaluate to type [int].


#3

A few goals

Let’s talk about what we want to achieve in some overhauls to FFL. When FFL runs, there three kinds of problems it can encounter:

  • Compile-time errors also known as static errors. These occur when the FFL is parsed. This is typically when an object type is loaded into memory, generally the first time you enter a level that uses one of those objects.
  • Run-time errors also known as dynamic errors. These occur when FFL is executed and does something illegal.
  • Incorrect behavior. This occurs when FFL is running, but the FFL runs without anything the engine considers an error, so execution of the game continues. However, it executes the wrong behavior. Maybe Frogatto starts flashing and doesn’t stop flashing when he’s meant to. Maybe an object disappears off the screen. In any case, it’s not what we expect.

Right now we have plenty of errors of each type. For instance, if you don’t have the right number of parentheses in your formula, you will get a compile-time error. If you try to set a variable to an invalid type, you will get a run-time error. And there are plenty of ways to get incorrect behavior.

The biggest premise of our improvements are that compile-time errors are better than run-time errors and incorrect behavior. Why is this? Compile-time errors are consistent and reproducible. They will happen right up-front and you can fix them. It’s even easy to make a special flag in Anura that will load all objects up-front to ensure that you get every single compile-time error.

You can argue whether a certain situation should result in a run-time error (halting the game) or if the engine should make a best effort to continue and likely cause incorrect behavior. However, a compile-time error is strictly better than either. A run-time error always has risk of showing up for customers, while a compile-time error will be weeded out immediately and fixed.

The system I’m working on aims to give some basic strong guarantees that an FFL developer can rely on. As a coder it’s so relieving when I can think “I’m not sure if I’m doing the exact right thing here, but I know if I’m not I will get a compile error, and then will just fix it.” I do this all the time in C++ and it’s a big reason I can be so productive in C++. When programming in a language like Python – or FFL in its current form – I feel so much more stress to remember “can I really do this? If I’m wrong I’ll end up with some runtime error that will show up who knows when?”

Fixing the dot operator

In C++, when I see level.character.hitpoints I know that I am safe. When I see level->character->hitpoints I think “uh-oh, potential problems. Am I sure this is safe?” Why is this? Because anything you can use “.” on in C++ can’t possibly be NULL, and the symbols will be checked, so I know level.character.hitpoints will work fine. lThe -> operator works on pointers, though, and they might be NULL. So level->character->hitpoints has two possible points of runtime failure.

In FFL, right now if you go level.character.hitpoints what will happen? You might get a valid value (presumably an integer) but you might get null, if hitpoints isn’t a symbol in level.character. Or you might get a run time error, if level or level.character aren’t valid. That’s two more possible outcomes than I’d really like. So, the biggest single improvement I want to make to FFL is that so once it’s passed compile-time validation, you can be absolutely sure that level.character.hitpoints will successfully return the number of hitpoints – and if this expression isn’t valid for some reason, you’ll get a sensible error message.

I want this to occur for every possible expression. As a programmer, worrying about symbol lookups like this being safe should not have to occupy your thoughts while coding.

The second biggest improvement is compile-time checking of function arguments. If you call filter(level.characters, value.hitpoints > 5) you should get a compile-time error if you aren’t passing the correct types of objects to the filter() function. The arguments you provide to a function will be checked when FFL is compiled, and this will give you strong assurance that function will behave as you expect.

What we need to do

Why is this all hard? It’s hard because now every single FFL object has to expose information about the types it presents for use. FFL has to know, at compile time, that ‘level’ will evaluate to something that contains a ‘character’ and ‘level.character’ will evaluate to something that contains ‘hitpoints’. It means we have to change the way we define objects and declare functions to accommodate.

The biggest change involves changes to how objects will store their state. Previously we’ve used tmp and vars as containers for variables, while we’ve used properties to implement dynamic behavior and give objects member functions.

Our current tmp and vars have several problems:

  • Distinct containers called ‘tmp’ and ‘vars’ based on the persistence pattern of the variables within seems like bad design.
  • These variables are accessible from other objects and so provide no support for data hiding and encapsulation
  • tmp and vars do not embed any type information
  • It’s hard to customize the behavior of variables later if we decide we don’t like their behavior – we have to go through and change every call site that accesses them.

New Style Properties

I’m simply going to deprecate tmp and vars and have implemented a bunch of new features in properties which allow properties to serve a wide range of data storage and functionality needs.

Properties have been fundamentally overhauled to allow for any and all cases where you want to store a symbol in an object. I’ll follow up on how new properties work in my next post.


#4

Properties

To begin with, let’s talk a little about what you could do with properties under the existing system. The biggest use of properties was to define complex calculations and especially functions. e.g.

calculate_xpos: "(x + img_w/2) * scaling",
move_obj: "def(num_iterations) map(range(num_iterations), do_movement(n))"

This all works wonderfully well, and the only change is that now we decorate the properties with types:

calculate_xpos: "int :: (x + img_w/2) * scaling",
move_obj: "def(int num_iterations) ->commands map(range(num_iterations), do_movement(n))"

I will explain what the “::” in calculate_xpos means in a later post. Also, for non-function properties you may be able to not define a type and Anura will try to infer the type of the property. All function properties will require full type definitions in strict mode and fail without them.

Now, I introduced the concept of ‘setting’ a property. You could have properties that would let you calculate something, so why not allow setting them? For instance, if you had,

my_xpos: "x + 5"

Why not let people set my_xpos directly as well?

my_xpos: { get: "x + 5", set: "set(x, value-5)" }

This works reasonably nicely. Now, to an external user, my_xpos appears just like another object property.

However, developing an object we now might desire to create a property that has actual storage backing. We don’t want a mere vars or tmp, because we might want custom, dynamic behavior. We would create something like this:

vars: {
  energy: 100
},

properties: {
  energy: { get: "vars.energy", set: "set(vars.energy, value)" }
}

Now that we’ve done this, we can expect everyone to access through the energy property. Then we have our energy concept nicely encapsulated. Later we could change behavior, like clamp energy to a range, or fire an event every time energy is changed, and so forth.

vars: {
  energy: 100
},

properties: {
  energy: { get: "vars.energy", set: "[set(vars.energy, max(0, value)), fire_event('energy_changed')]" }

Properties like this are very nice for their flexibility. Unfortunately, they have several shortcomings. They are annoying to define, since you have to define the vars and property separately. Also someone might access vars.energy directly, not through the property, which would circumvent the encapsulation.

Instead, you can now define such a property like this:

  energy: { variable: true, default: 100, get: "_data", set: "[set(_data, max(0, value)), fire_event('energy_changed')]" }

What is this “_data” symbol? Any property that has variable: true gets its own special _data symbol. The _data symbol is private to that property – so each property’s _data is distinct. It’s what it can use to store the data related to the property.

Also, we like convenient defaults. Most of the time once you have your property defined in a {} you probably want it to be a variable, so variable is true by default. Also, get defaults to “_data” and set to “set(_data, value)” so you might not have to define them. In fact, you can just define it like this:

energy: { default: 100 }

Except now with this new strict typing, we do want to have a type to tell us what energy is. So we do this:

energy: { default: 100, type: "int" }

You can, however go real simple:

energy: 100

The engine will assume that since 100 is an int, you want energy to be an int, with default value 100, and you want it to be a variable with appropriate get/set. It’ll do this for other primitives, such as booleans, decimals, etc. It won’t work for strings, since if you provide a string, it’s assumed to be a formula defining the property, not a string variable.

Something to note here – we’ve exposed what is effectively a variable publicly for anyone to modify. Does this mean our object is poorly encapsulated? No! The encapsulation is actually very good, because though other objects can modify our energy, they don’t have to know how energy is implemented. They don’t know if it’s just a simple variable, or if it has special behavior when modified, or is implemented in some other way. We can change the implementation of energy without any objects that access energy changing at all.

We can, however, make energy private:

energy: { default: 100, type: "int", access: "private" }

This way it can’t be accessed outside our object. It’s nice to have a naming convention where private data members are named differently though, so by default symbols beginning with an underscore are private by default. e.g.

_energy: { default: 100, type: "int" } //this is now private

What about tmp? We want objects that don’t get saved when the object does. This is simple enough:

energy: { default: 100, type: "int", persistent: false }

The default setting is a “hard-coded” FSON value. What if we want something a little more dynamic to initialize a property with? You can use init to do this:

energy: { init: "level.player.energy_count", type: "int" }

This formula will be run whenever the object is constructed as the initial value for this property.

What if we wanted to be a little more generous about how somebody could set the energy property. For instance, we wanted to allow some string values:

set(energy, "full") //set it to 100
set(energy, "half") //set it to 50
set(energy, "empty") //set it to 0

We’ll never return string values, just accept them. We can do this:

energy: { set_type: "string|int", type: "int", set: "set(_data, switch(value, 'full', 100, 'half', 50, 'empty', 0, value asserting value is int))" }

This lets us define a distinct type when setting the property vs getting it. By default, set_type is equal to type, but may explicitly be set differently. Note that both _data and the value returned from get must be of the type given by type.

Sometimes it is desirable for an object to have some data that must be provided by the code calling the object. For instance, suppose you were creating a lever object and you want the lever object to have a gate object attached to it that will be opened when the lever is pressed. You might define your property like this:

gate: { type: "null|obj gate" }

Note that the type being set to “null|obj gate” means the gate can be null. “obj gate” is the type of a custom_object of type gate. ‘default’ is null by default, so the gate will start life as null. If we want it to never be null, we might do this:

gate: { type: "obj gate" }

This will guarantee (when running in strict mode) that gate always actually points to a gate object. Now, when you create a lever object:

object('lever', x, y, facing, { gate: mygate })

That gate: mygate part MUST be included. and it must be of type obj gate. If it’s not you’ll get an error. (which will most likely be at compile time, though it may be at run time when constructing the gate because of reasons). The lever object will never come into existence unless it’s given a gate.

Likewise, I’ve changed the spawn() function to accommodate:

spawn('lever', x, y, {facing: facing, gate: mygate})

Note that the fourth argument used to be for ‘facing’. Now it takes a map of properties, which can include facing. The fifth optional argument for spawn() can still take a list of commands like before. We don’t want to initialize gate there though because by the time those commands start executing the object has already been created. What if one of the commands accessed gate before the command to set the gate? That’s why we use a map to do it – it allows FFL to inspect and verify that all needed properties are being initialized and that this is done before access of them might be needed.

Sometimes it’s possible you might not be able to initialize properties like this. What if you also wanted your gate to have a reference to the lever that accesses it? You’d have to create one of the objects first. So, if using the map to initialize a property doesn’t work out, but we still want a property which can’t be defaulted to something, we can use an override flag:

gate: { type: "obj gate", dynamic_initialization: true }

This tells the engine "I want to initialize this property myself. I promise I will initialize it so a valid obj gate as soon as it’s created (i.e. in on_create, on_spawned, the spawn initializer list or in FFL after object() returns but before calling add_object()).

The engine will check when the object is added that you did initialize the property and throw a runtime error otherwise. It’ll also throw a runtime error if the property is read at any time before being initialized.

This covers most of the new features of properties. In my next post I’ll cover the details of how FFL’s new type system actually works.


#5

The primary goal of FFL’s type system is to ensure that every FFL expression has a definite type, that is known when the formula is parsed. By knowing the type of an expression, we know if the way it is being used is legal, and can report appropriate error messages.

To allow this to occur, symbols in FFL must have types associated with them. For instance, if we have this expression:

a + b

What is its type? It depends on the types of ‘a’ and ‘b’. If a and b are strings, the expression will be a string, if a and b are ints, the expression will be an int.

This is why, in strict mode, you have to specify a type for properties. So when properties are used, the compiler will know what type they are.

Types are organized into an indefinite hierarchy, with the type any at the top of the hierarchy. If FFL can’t determine what type something is, it will be deemed to be type any. Primitive types – ints, strings, decimals, and so forth, are all distinct types.

But I said an indefinite hierarchy, didn’t I? What do I mean by that? We define a type, Numeric, to be all ints and all decimals. So really, int < Numeric < any.

(I use ‘<’ in this post between types to mean ‘subset of’)

Numeric is not specially defined by the engine, it’s just a user-defined type. We define it in types.cfg like this:

Numeric: "int|decimal"

This is called a type union. A type that might be an int or a decimal. You don’t need to use types.cfg to use type unions, though, it’s just a convenience, you could write a function like this:

square: "def(decimal|int num) -> decimal|int num*num"

Note that FFL is smart enough to infer the result type of any expression. For instance, if you instead wrote,

square: "def(decimal|int num) -> decimal num*num"

You’d get a compile time error in strict mode telling you that you claimed your function evaluates to decimal, but after parsing it, FFL discovered it might actually be an int, so decimal|int is the correct result type.

An important thing to remember when dealing with types is the more specific a type is, the more useful it is. any can be used to describe any expression, but it’s not very useful. If you have a value of type any, you’ll be allowed to do very little with it, because there’s very little you can safely do with it.

More on types.cfg and types: {}

You can define types in two places. In a module’s data/types.cfg and a custom_object can define types to be used within its definition in a types: {} block. As an example (will make more sense as you read on in this…)

types: {
	Numeric: "int|decimal",
	Loc: "[int,int]",
	DamageMessage: "{source: string, damage: int, target: string}"
}

Note that types defined like this must being with a capital letter to be recognized.

Custom objects

So what type is a custom object, such as Frogatto’s object? All “Objects”, that is, complex C++ objects with an FFL interface are considered to be of type “object”. However, custom objects are of type “custom_obj” which is a child of object. Then, Frogatto’s object is of type “obj frogatto_playable”. frogatto_playable is based on the playable prototype, though, so it’s also of type “obj playable”.

So there’s a big long hierarchy going on: obj frogatto_playable < obj playable < custom_obj < object < any

Now, remember, in strict mode you will only be allowed to do things that FFL knows will definitely be successful. Let’s say you have a reference to Frogatto’s object. What can you legally do with it based on the type of the reference?

  • If you have an ‘any’ you can only pass it to functions that accept an any, or use type discoverability to try to work out a more specific type (see more on that later).
  • If you have an ‘object’ you can look up symbols inside the object, e.g. frog.hitpoints, but the symbol will be looked up at runtime and the type of frog.hitpoints will be ‘any’. (Since the engine has no idea what type it actually is). Note that generic ‘object’ types are a little unsafe even in strict mode, in that you can look up arbitrary symbols and if you look something up that’s not present you’ll get a run time error. I’m considering forbidding symbol lookups on generics objects.
  • If you have a ‘custom_obj’ you can look up symbols that are common to all custom objects, such as hitpoints or x and y and they will have full type information.
  • If you have a obj playable you can look up symbols defined in the playable type.
  • If you have a obj frogatto_playable you can look up symbols defined in frogatto_playable.

The null type

In most languages, references (or pointers) to objects can also have the value null. In FFL, we do things a little differently. null is a type of its own, and values of type null can have only one possible value – null. So ‘null’ is the name both of a value and the type that value is. Have you ever seen Frogatto spew the error “Expected type string but found null null”? Know why there is the redundancy of nulls there? Because the engine outputs the type and then the value. It might say “Expected type string but found int 5”. But when the value is null it’ll output the type – null – and the value – null.

This means that if you have a symbol of e.g. type ‘obj frogatto_playable’ you know that this will NOT be null. It will definitely have a frogatto_playable object. Thus a function such as def(obj frogatto_playable frog) must be passed a frogatto_playable as an argument. null is not a valid argument.

If it’s possible for a symbol to be null, use a union with the null type. e.g. def(null|obj frogatto_playable frog) allows ‘frog’ to be passed as null.

However, in strict mode, FFL will not allow you to lookup symbols in a reference that may be null. Using frog.hitpoints inside this function will cause an error, since ‘frog’ might be null.

To lookup symbols in frog, you must convince FFL that frog is not null. You do this with type discoverability.

Type discoverability

Inside a formula, types of symbols are not necessarily static. FFL analyzes the structure of a formula and infers as much information as it can about the type of symbols within different branches of the formula. Let’s take a simple example of some bad code:

get_life: "def(null|obj frogatto_playable frog) -> int
  frog.hitpoints"

Strict mode will give you an error here. “frog might be null”. It’ll refuse to go on. How do we placate it? Like this:

get_life: "def(null|obj frogatto_playable frog) -> int
  if(frog != null, frog.hitpoints, 0)"

That’s sensible coding, even without the type system. If frog might be null, protect the symbol lookup with an if statement. What’s nice is that FFL sees what you did there and while it has frog’s type down as ‘null|obj frogatto_playable’ in the ‘then’ branch of the if statement it automatically modifies the type of ‘frog’, making it a ‘obj frogatto_playable’.

It’s smart enough to know about things like and and or and so will parse this legally:

get_life: "def(null|obj frogatto_playable frog) -> int
  if(frog != null and frog.hitpoints > 0, frog.hitpoints, 0)"

Since the ‘frog.hitpoints > 0’ lookup came after and ‘and’ where we checked frog isn’t null, FFL will recognize that this is legal and frog.hitpoints is guaranteed to work.

As well as guarding things with if statements, it also recognizes asserts:

get_life: "def(null|obj frogatto_playable frog) -> int
  frog.hitpoints asserting frog"

It recognizes that if frog is null, it’ll die in the assert, so frog must not be null and thus it infers frog’s type as “obj frogatto_playable”, without the null.

These mechanisms allow you to fairly easily prove to FFL that something isn’t null, and is generally good code that you should be writing anyhow.

Of course, sometimes you want to discover types in more sophisticated ways – not just null vs not null. So, FFL provides the is operator. The is operator tells you if an expression is of a given type:

if(message is string, set(dialog.text, message))

Again, FFL is smart enough to infer, within the ‘then’ branch of the if statement, that message must be of type string.

Use of the is operator is powerful, but it should be used judiciously, overuse can result in complex and unmaintainable code.

Here are some examples of common idioms which you’ll find useful in dealing with the type system:

map(filter(level.chars, value is obj mywidget), value.do_operation())

Note that the filter() is smart enough to realize that its result is a [obj mywidget]. Thus even if do_operation() is only available in 'mywidget’s definition, FFL will realize this is a legal expression.

Alternatively this can be written as a list comprehension:

[mywidget.do_operation() | mywidget <- level.chars, mywidget is obj mywidget]

Note that list comprehensions are particularly powerful at this, because of the way they infer types. Let’s suppose you wanted to do some operation on all ‘mywidgets’ that passed some certain criteria.

[mywidget.do_operation() | mywidget <- level.chars, mywidget is obj mywidget, mywidget.is_good()]

Note that in the mywidget.is_good() expression, FFL has already worked out that mywidget is of type ‘obj mywidget’ and so can successfully resolve the is_good symbol lookup.

We also have a new find_or_die() function which is nice if you just know you should find something:

find_or_die(level.chars, value is obj frogatto_playable).spit()

find_or_die() will know, in this case, that it’s returning an obj frogatto_playable. Regular find() would return type null|obj frogatto_playable.

The :: and <- operators

FFL provides two operators for forcing conversions. These are the :: and <- operators. The :: operator is very useful for working out formula errors, for defining properties, and in general is always safe. It only affects the compile process and has no effect at run time. The <- operator is a run-time operation and can result in a run-time error. It should be used judiciously.

How do they work then? The :: operator is really simple. It just says “This expression is of type T” the FFL compiler will compile it and if it disagrees, will raise an error. Examples:

level_width: "int :: level.dimensions[2]" //define a level_width property of type int
decimal :: (5 + 4.0)/2  //I think this will result in a decimal -- right?

This is useful sometimes when you’re getting a compile error you don’t understand. Add it to expressions to ensure the compiler agrees they are the type you think they are. Also, this is the way to explicitly state the type of an object property. Often it’s required to do this for an object property since the compiler might not be able to infer the type when it’s first required.

The <- operator is used when you just know that an expression evaluates to a certain type, but FFL really can’t figure it out. For instance,

5 + (int <- a)

This basically says “look I know you have a down as type any but it’s actually going to be an int. For sure. I’m the coder. Trust me.”

FFL will take your word for it, compile the expression as an int. Then if it turns out to be something different at run time you will get a run time error saying so.

Sometimes using <- is necessary – and will be especially so while our support for types is still maturing – but should eventually become more and more obscure. If you’re using it too often there’s a sign something is wrong. Using :: is much nicer and safer!

An example of when <- is definitely necessary is if you receive a FSON tree from an unknown source. For instance, loading it from the user’s preferences directory with get_document(). You’d want to use <- to tell the code to load and validate it into some kind of type you provide. If it’s not in the correct format you’ll get a run time error – which is the exact reasonable behavior you’d expect when loading a file at runtime that is corrupt.

Lists

FFL’s type system supports two kinds of sequences: the more general lists, and the more specific tuples. A list is simply a sequence of a certain type and can be any size. Examples of lists:

list //a list of any type and size
[any] //same as above
[int] //a list of integers
[int|string] //a list of integers or strings
[custom_obj] //a list of custom objects
[obj ant_black] // a list of ant_black custom objects
[null|obj ant_black] //a list of values that can be ant_black custom objects or null
[[int]] //a list of list of ints

A tuple is a list of a certain size, with well-defined types for each element. For instance,

[int,int] //a tuple of 2 elements, both ints
[string,int,int] //a tuple of 3 elements -- a string, then int, then another int.
[int,] //a tuple of exactly one integer. Note the ',' disambiguates it from [int] which would be a list.
[int|string,int|string] //a tuple of two elements, each of which can be an int or a string.

The thing to understand about tuples is that they are considered a subset of lists. That is to say, if you have a [int,int] it will be happily accepted anywhere an [int] is required. If you have a [int,int,decimal] it will be accepted anywhere an [int|decimal] is required.

The big rationale for this is to allow you to continue to write list constants the way you always have. For instance, if you write,

What is that exactly? Maybe it’s just a list that could be any length, or maybe it’s specifically three items because it’s meant to be in 3D co-ordinate space and is going to be used as a tuple that must be three elements. We don’t really know. We could force writing of list literals differently, so that you have to identify more about them, but I think that would be more burden on the coder.

So instead, that expression is treated by FFL as a [int,int,int]. But FFL will happily degrade it to a [int] implicitly.

Tuples do allow things like this, which we use in Citadel:

//in types.cfg
Loc: "[int,int]"

And then we have functions which take these Loc types. They will be validated so they must be tuples exactly as given. Note that the conversion does not go the other way – a [int] can only be converted to a [int,int] with the <- operator.

Maps

We have something similar going on with maps. Except I don’t have a good name for the ‘tuple version’ of maps, so I call them ‘specific maps’. They could be called structs, I suppose.

//Examples of maps
{int -> string}
{string -> int|null} 
{string -> {string -> [int]}}

Maps are designed for cases where you might want lots of different keys and you don’t know how many keys you will have or what they will be called when you’re writing the code.

However, there are many cases where you use maps differently. You want to write a map like { name: “Frogatto”, damage: 17, speed: 24 } – and you know that these keys, name, damage, and speed will always be present.

Such a map is a ‘specific map’. This is how we’d write the definition:

{name: string, damage: int, speed: int}

Note the use of ‘:’ instead of ‘->’. This is what differentiates a specific map from a map.

You can actually provide for some keys to be omitted, to do so make it so that the value can be null. For instance, if you want to make speed optional, do this,

{name: string, damage: int, speed: int|null}

As with tuples, map literals are inferred as specific maps, but will happily degrade into maps implicitly.

Note that get_document() will try to resolve at compile time if you give it a constant string filename that’s not in a user directory, giving you full access to the document structure and automatically reading the type as a specific map.

Specific maps allow you to very easily create your own little types that you can put in types.cfg or an object’s types section.


#6

Interfaces

FFL supports a special type called an interface. An interface is used to implement Duck Typing in a type safe way.

Let’s suppose you wanted to write a generic function to find the manhattan distance between two points. But what can be passed in as a ‘point’? Interfaces allow you to say “you can pass in anything you want, as long as it has members x and y, both numeric”. So, you define your function like this:

manhattan_distance: "def(interface {x: numeric, y: numeric} a, interface {x: numeric, y: numeric} b) -> numeric abs(a.x-b.x) + abs(a.y-b.y)"

Of course, this is very verbose. So it’s likely that you’d want to define something in types.cfg or the types: { … } section of the object you’re editing:

// in types.cfg
PointInterface: "interface { x: numeric, y: numeric }"

// function definition
manhattan_distance: "def(PointInterface a, PointInterface b) -> numeric abs(a.x-b.x) + abs(a.y-b.y)"

Now, anything that provides x and y as numeric types can be passed to our manhattan distance function. We could pass a map, a custom_object (since they have x and y attributes) or any other object that happens to have x and y.

Note that it must be known that the interface matches the passed in type at compile time. This means that interfaces are unlikely to be usable in non-strict mode. They are designed for strict mode when we know all the types.

The only places you can use interfaces are as parameters to functions or with the :: operator. You can, for instance, write this:

where mypoint = PointInterface :: { x: 5, y: 8}  //Nice safe use of an interface

This will verify, at compile time, that the expression to the right of the :: matches the interface, and will convert it to this interface. Note that conversions to interfaces always take place at compile-time, never at run-time. If you write this:

where mypoint = PointInterface <- some_expression  //This is generally NOT a good idea.

Then some_expression must evaluate to something that is a PointInterface. An object that is convertible to a PointInterface isn’t good enough. In general it’s unlikely you want to do this. Only use interfaces as function arguments and with ::.

If you don’t understand the differences between :: and <- read about them above. Remember, :: is good and safe, while <- is high in fat, high in calories, and generally bad for you.

Interfaces are designed to be an important part of FFL’s support for type-safe generics. Unlike most of the rest of the type system presented so far, I haven’t had much of a chance to use them in practice yet, so I hope they prove to be useful!