How to Avoid Repetitive Code in Fearless

You may already have noticed that even relatively simple programs have the potential to be very long. Programs which you may use daily (such as Steam, Zoom, TikTok, and Instagram) are composed of millions of lines of code. Some programming languages force the user to write very repetitive code; either by literally repeating the same text over and over again, or by repeating similar but slightly different code. Repetitive code is bad and Fearless has ways to avoid repeated code and promote code reuse.

We have already seen three ways fearless avoids repetitive and redundant code.

We now introduce the concept of syntactic sugar. As for method calls, method inheritance and type inference, also syntactic sugar is designed to avoid redundant code.

Syntactic sugar allows representing specific well known coding patterns using more concise and more readable syntax.

Syntactic Sugar does not change the meaning, it just provides a shorter way to write the exact same thing. Think of it like a contraction in English: "don't" instead of "do not". It is shorter, but the underlying meaning is identical.

We will now see how a combination of syntactic sugar and inference can make the code for Direction even more compact. The only abstract method in Direction is .turn, so when implementing direction it is obvious that we want to implement .turn. In this way, the syntactic sugar allows us to write the following, shorter version of the code we have seen before. We can omit .turn-> when implementing a single method to satisfy Direction, as shown below.

Direction: {
  .turn: Direction,
  .reverse: Direction -> this.turn.turn,
  }
North: Direction { East  }
East : Direction { South }
South: Direction { West  }
West : Direction { North }

Understanding the code above

  1. North: Direction { .turn -> East, } is a type declaration.
  2. North: Direction { East } is a more compact form of the same type declaration.
  3. .turn: Direction is a method declaration.
  4. .turn -> East, is a method implementation.
  5. East is an example of an object literal expression.
  6. North.turn is an example of a method call expression.
  7. this is an example of a parameter.
  8. North and East are valid object literals because they have no abstract methods.

We will later see that both method calls and object literals can be much more involved than the ones shown in our examples so far.

Object literal: inheritance plus syntactic sugar

Object literals work as shortcuts to avoid repeating the full code they represent. Using literals as names referring to a type declaration is another way to reuse code. When writing North we are making heavy use of syntactic sugar. North actually stands for SomeName147: North {}, where SomeName147 is some name chosen by the syntactic sugar to be different from any other name present anywhere else in the code.

Thus, when writing North we are declaring a version of North, called for example SomeName147 that inherits all of the methods of North and does not add any new methods (the empty curly brackets {}). The type declaration SomeName147: North {} produces an object of type SomeName147 when evaluated. Since SomeName147 implements North, any object of type SomeName147 is also of type North.

In this way, the object literal North evaluates into a standard object of type North, with all its defined methods.

Example: Tank with turret

Now that we have the abstract type Direction we can make a simple Tank object. This tank will have two Directions;

Tank: {
  .heading: Direction,
  .aiming: Direction,
  }
TankNN: Tank{ .heading-> North, .aiming-> North,}
TankNE: Tank{ .heading-> North, .aiming-> East, }
TankNS: Tank{ .heading-> North, .aiming-> South,}
TankNW: Tank{ .heading-> North, .aiming-> West, }
TankEN: Tank{ .heading-> East,  .aiming-> North,}
...//16 cases in total!
TankWW: Tank{ .heading-> West,  .aiming-> West, }

As you can see, while it is possible to manually list all sixteen cases as valid Fearless code, the development process is boring, repetitive and error prone: it's incredibly easy to make a typo. What if we accidentally include .aiming-> North five times instead of four?

Would not it be better if we could just ask for a Tank with a specific heading and aiming direction when we need one? We can create a Tank maker by defining a type Tanks whose job is to create Tank objects for us. We give it the details (heading and aiming), and it gives us back the specific Tank object we need.

Side note: This pattern of having one type create instances of another is common. Naming the maker type by pluralising the product type (Tanks for Tank) is a Fearless convention. It's concise and hints at its role.

Tanks: { .of(heading: Direction, aiming: Direction): Tank ->
  Tank: { .heading: Direction -> heading, .aiming: Direction -> aiming, }
  }

As you can see, we have moved the declaration for Tank inside of a method body. Let's break down this piece of code carefully.

Method parameters: Providing input

Look at the part inside the parentheses: (heading: Direction, aiming: Direction). This declares parameters for the .of method. Parameters are how we pass information into a method when we call it. heading: Direction declares a parameter named heading. The : Direction part specifies that whoever calls .of must provide a Direction for this parameter. aiming: Direction similarly declares an aiming parameter, which also must be a Direction. You've already seen the implicit this parameter. Parameters listed in parentheses like this are explicit parameters - we give them names and types directly.

The .of method expects two inputs: a Direction for the heading and a Direction for the aiming. Directly after the parenthesis and before the arrow -> we can see : Tank. This is the return type and it tells us the result must be a Tank object.

The method body: Creating the tank

Now look at the part after the arrow ->:

Tank: { .heading: Direction -> heading, .aiming: Direction -> aiming, }

This is the method body: the code that executes when Tanks.of is called. It defines what the method does. What does Tank: { ... } mean here, inside a method body? It looks like our earlier Tank type definition, but it is now also an object literal. In addition of defining the Tank type, it also serves as a template for Tank objects. The execution of Tanks.of will evaluate this object literal and thus return a Tank object customised with heading and aiming.

Using the parameters: Notice how heading and aiming (the parameter names) appear inside this object definition:

Capturing values: The Tank object returned by .of captures (or remembers) the specific heading and aiming values that were provided when .of was called. If we call Tanks.of(North, East), the object created will be a Tank heading North and aiming East. If we call Tanks.of(South, West), it will be a Tank heading South and aiming West. This allows us to create Tank objects with custom, specific states based on the inputs we give to the .of method. We have moved from having only a few fixed Direction objects to being able to create many different Tank objects, each remembering its own specific heading and aiming.

To summarise, Tank: { ... -> heading, ... } is a form of object literal expression; it's a way to define the structure of a type and forge an object of that type in one step, often using captured parameter values to customise it.

Method Declaration Syntax

Methods can have as many parameters as we want. Syntactically, explicit method parameters are defined inside of round brackets. When there are no explicit parameters, these brackets can be optionally omitted / left out. For example, the methods .turn and .reverse take no explicit parameters, so before we omitted the parenthesis. These same methods could equivalently be declared as .turn() and .reverse(). To call the method .turn twice we showed the syntax North.turn.turn but we could have equivalently called it with syntax North.turn().turn(). The syntactic sugar of Fearless allows us to include or omit most empty brackets.

This newly shown method can be called with syntax: Tanks.of(North, East) Here Tanks is the first implicit parameter and it is called the receiver. The other are provided after the method name in parenthesis.

The syntax .of(heading: Direction, aiming: Direction): Tank defines a method called .of with parameters heading and aiming. The parameters must be kinds of Direction and the method must return a Tank object. Parameters allow methods to take input. In this case, our Tanks.of method requires us (the developer) to specify the initial heading and aiming of the tank we want to create.

Example questions:

That is, a receiver can be any expression, not just an object literal.

That is, also method parameters can be any expressions.

English to Fearless Conversion

Fearless code can be understood by aligning over it some natural language. Consider the following example where we will describe some desired features of our Tanks code in English and then show how they can be implemented in Fearless code.

We would like the Tanks.of method to create a Tank object.
This Tank will have two methods: .heading and .aiming.
Tank.heading will return the direction the Tank is heading;
and Tank.aiming will return the direction the Tank is aiming.

  Tanks: { .of(heading: Direction, aiming: Direction): Tank ->
    Tank: { .heading: Direction -> heading, .aiming: Direction -> aiming, }
    }

Tanks.of takes two directions (heading and aiming) and returns a newly-created Tank object. The second line of our Fearless code above does this by using a type declaration for a type Tank inside the method body.

You may think that declaring a new type inside of the method body contradicts the general idea that method bodies must be expressions. However, an object literal expression is just a special kind of type declaration. Before we have directly used type names, like North, as a object literals. As we discussed, the object literal North is desugared intoSomeName147: North {}. All object literals are type declarations. Some object literals do not look like type declarations because of the sugar allowing to omit SomeName147: and because of the general rule that empty parenthesis like {} can be omitted.

This use of type declarations as objects is interesting because we can create a new kind of object, an object able to see / capture the method parameters. In this way, objects can have a custom, useful state. Up to now we have worked with just a finite set of objects: the four directions. Using capturing, we can create more objects, and we can compose objects to obtain new objects. For example, we could define a Platoon object containing many individual Tank objects.

In the code above we defined Tank directly in the Tanks.of method. In this way, the only way to create Tank objects is to call Tanks.of. Alternatively, we can keep the Tank declaration outside and use inheritance as shown below:

Tank: { .heading: Direction, .aiming: Direction,}
Tanks: { .of(heading: Direction, aiming: Direction): Tank->
  MadeTank: Tank { .heading -> heading, .aiming -> aiming,}
  }
TankNN: Tank {.heading -> North, .aiming -> North,}

In this way we have more freedom to create Tanks: via the Tanks.of method or via a top level declaration, like TankNN.

To do so, we introduced the name MadeTank, to indicate tanks originating from that point in the code. The name MadeTank is not very useful, we will probably never want to talk only about tanks made with the Tanks.of method, so we can rely on the sugar and type inference to chose a name for us and to infer that the literal we are creating is extending Tank. In this case, the name for our literal is going to be some fresh name that never appears anywhere in the code. That is, the code below is an equivalent but shorter version of the code above.

Tank: { .heading: Direction, .aiming: Direction,}
Tanks: { .of(heading: Direction, aiming: Direction): Tank->
  {.heading -> heading, .aiming -> aiming,}
  }
TankNN: Tank {.heading -> North, .aiming -> North,}

In practice, while coding in Fearless, most literals will rely on inference and will be written just as {..}.

The three kinds of expressions, revisited.

We have now seen more examples for the three kinds of expressions:

Method bodies are expressions, so any method body will be exactly one expression. Expressions can have sub expressions: Tanks.of(North,East.reverse) has sub expressions Tanks, North and East.reverse. In turn East.reverse has sub-expression East.

Object literal expressions are also type declarations, and they can internally contain method declarations.

Declarations and expressions

In Fearless there are two contexts: declarations and expressions.

As you can see, the two contexts are interleaved inside each other.

Method names

At this point you must have noticed that all the method names we have show start with .; and you may be wondering why the odd choice. The . allows the computer to separate method names from other kinds of names; for example in North.turn or Tanks.of it is clear where the type name finishes and the method name starts. Fearless has two kinds of method names:

The full list of operator symbols is:

  ! ~ # & ^ + - * / < > = :

However, since the : is already used to mean "declaration", an operator symbol can not be just :.

That is, the following is a list of valid and invalid method names:

.foo  #  :=  ++  <=  .bar23  <#--  <+:  //valid
.foo+  +bar  a=b  zoo  <hello>  .+>  :  //invalid

In turn, this means that the code below is syntactically valid

Bar#(Add:-)

Adding spaces around all tokens this would look as follows:

Bar # ( Add :- )

This is the a call of the method called # on the receiver Bar, and the single parameter is a call of the method called :- on the receiver Add. Method :- takes zero parameters.

On the other side, parameter names start with a lower-case letter, and type names mostly start with an upper-case letter. The rules for valid type names are a little more involved, and we will discuss them in detail later.

While the code above works fine, we think that using .of in this way is verbose and distracting: is .of the right method name? Conceptually we just want to do Tanks, go!!! do your thing! be!

It is very common for Fearless types to have a method that is the most crucial method of that type. As a convention, we use the method called # for that role. The operator # is much more compact to call than the method .of.

Rewriting our last code example using # we get the following:

Direction: {
  .turn: Direction,
  .reverse: Direction -> this.turn.turn,
  }
North: Direction { East  }
East : Direction { South }
South: Direction { West  }
West : Direction { North }

Tank: {.heading: Direction, .aiming: Direction, }
Tanks: { #(heading: Direction, aiming: Direction): Tank ->
  { .heading -> heading, .aiming -> aiming, }
  }

Note how pretty much nothing has changed. We just declared the method to be called # instead of .of.

We could have renamed into # also the method Direction.turn, but we chose not to: in our mental model we do not think that turning is the main thing we do with directions. When coding it is important to distinguish the intended/ideal state of the code from the current incomplete version that we are working with, and to name concepts in function of such ideal state and not the current sorry version of it.

We can now consider adding some methods to Tank: for example the capacity of turning the turret! To do so, we only need to update the code of the type declaration for Tank:

Tank: {
  .heading: Direction,
  .aiming: Direction,
  .turnTurret: Tank-> Tanks#(this.heading, this.aiming.turn)
}

As you can see we declare a new method .turnTurret that returns a Tank with the turned turret. We do this by calling Tanks# with the current heading direction and the result of turning the current aiming direction. We can call this method as follows:

Tanks#(North,East).turnTurret

This will reduce as follows:

Tanks#(North, East).turnTurret
Call Tanks#
Tank{.heading ->North, .aiming ->East,}.turnTurret
Call Tank.turnTurret

Tanks#(
  Tank{.heading ->North, .aiming ->East,}.heading,
  Tank{.heading ->North, .aiming ->East,}.aiming.turn
  )
      
Call .heading

Tanks#(
  North,
  Tank{.heading ->North, .aiming ->East,}.aiming.turn
  )
      
Call .aiming

Tanks#(
  North,
  East.turn
  )
      
Call East.turn
Tanks#(North, South)
Call Tanks#
Tank{.heading ->North, .aiming ->South,}
Final result

It is important to learn to visualise how the code reduces in your mind, so that you can predict code behaviour. Note how we wrote Tank{.heading -> North, .aiming -> South,}.

Inference works on source code: the code we write. Code under reduction is not source code, but just a tool for us to understand the code behaviour. Since it is just a tool, we can afford to relax and to rely on inference as much, or as little, as we want.