If you've gotten anywhere with programming, you'll have noticed that some people use some logic patterns that seem a bit "clustered", and you might think that they are being efficiency freaks and writing "hacky" code. That's not exactly the case. When you understand the logic patterns behind many of the operators available to you, you can begin to build habits that will help you avoid redundancies in your code and help you understand some of these "clustered messes" and "hacks" that get much derided around these parts.
Let's introduce you to DM's operators:
Order | Group | Operators |
---|---|---|
-2 | expansion | expansion |
-1 | lexical | access |
0 | (P) structural | ( ) |
1 | navigation | [] () . / : |
2 | single operand | ~ ! - ++ -- |
3 | (E) arithmetic | ** |
4 | (MD) arithmetic | * / % |
5 | (AS) arithmetic | + - |
6 | relational | < <= > >= |
7 | binary | << >> |
8 | relational | == != <> |
9 | binary | & ^ | |
10 | boolean * | && |
11 | boolean * | || |
12 | ternary | ? |
13 | assignment | = += -= /= &= |= ^= <<= >>= |
*: && and || operators are special conditional boolean operations sometimes called logical operators. See below for a discussion on short-circuiting
You may be familiar with PEMDAS or BEMDAS (depending on which side of the Atlantic you were born on). This is a standard mnemonic device that is drilled into the heads of every algebra student. It determines the order that you perform operations in an expression. Programming inherits and expands these rules quite a bit.
Every operator is sorted at compile-time using this priority list. The negative phases only matter if you care about how the compiler and interpreter works to resolve values. Everything else matters for what you actually type when compiling and determines the order that operations will occur in. The sequence from 0 to 13 can generally be assumed to be the order that operations will occur, and all operators that share a priority will generally be resolved left to right. There are some exceptions.
Operands: Values used in conjuction with operators are called operands. These are the values that the operator uses to perform its function. For instance, the operands of b*4 are "b" and 4.
Associativity: Associativity determines the sequence that operators are processed in. 4**2**3 is equal to (4**2)**3 because ** has left associativity. Meaning the operator will be processed from left to right. Multiplication and addition on the other hand are right and left associative. It doesn't matter what order they are processed in because the values will be the same regardless of what direction you process them in. In many languages, assignment operators are right associative. In DM, though, they are non-associative. What does this mean for you? Well, in many languages you can perform the operation a = b = 4*3, or a = b += 4. in DM, however, you cannot perform these tasks and must instead break the operations up into multiple lines.
Pre/post/infix: Some operators come before the operand. Some come after. Some operators come between two operands. The negation operator (-) is a prefix operator. It modifies the operand that comes after, performing an XOR operation on the negative bit of a numeric value. The ++ and -- operators are actually not a single operator, but two identical looking operators. This causes a lot of confusion because people believe these operators to be the same operator acting differently just because they look the same. This is not true. These symbols denote a different operation based on their position relative to an operand.
a++ or a-- is a different operation completely from ++a or --a. We'll get into this more in depth below.
Output values: Some operators take one or more operands and create an output value. 4*2, for instance takes two operands and converts it into a single output. In this case, 8. This output is then used for any subsequent operations in the expression. So 4*2+1 would be a three-step process:
4*2+1
8+1
9
Making incorrect assumptions:
When people first learn about operators, they tend to associate their operations with specific uses. For instance, most people think of the assignment class of operators as used for changing variables (stored memory values). This is actually true! The assignment class of operators has a singular use. Their behavior is exclusively for operating on memory values.
But then people make bad assumptions based on this same logic. Since assignment operators are only used for operating on memory values, therefore boolean and relational operators only belong inside of a conditional statement! This is not true! Almost every operator is equally valid in any non-constant expression! So if you thought that the conditional OR and conditional AND operators are only useful inside of an if statement, while statement, for statement, etc, prepare to learn some cool junk!
if(value1&&value2)
This is a common use of an && operator. It's being used to test if both value1 and value2 are true. You almost never see && and || operators outside of an if statement. But let's take a look at a more complex usage.
if(value1&&value2&&value3&&value4)
return 1
else
return 0
Is roughly the same as:
return value1&&value2&&value3&&value4
There is a difference you should be aware of before using this pattern everywhere. In the case that the values are not equal to 1 or 0, you should be aware that && outputs either zero or the rightmost value.
This means that:
if(a&&b)
return 1
else
return 0
will return 1 or 0
and:
return a&&b
will return b or 0
&& and || often function as boolean logic operators. This is why any pattern that suggests that you check if a boolean is equal or not equal to 0 or 1 is wrong. boolean logic operators do not always return 0 or 1. They generally do, but not always. Remember that 0 or null is always false, and any value except those two are always true. As such, numerics, strings, objects, and any other datatype are all operable by boolean logic. They are not boolean, but they will be treated as boolean for the purpose of boolean operations. This means that the correct way to evaluate a boolean function is never if(function()==0) or if(function()==1). You cannot and should not depend on a function being 0 or 1 even if the reference tells you that it only returns 0 or 1. It's both wasteful and unsafe. You must depend ironically on the function being considered boolean, but returning only a virtually boolean value, which means the correct way to check them are if(!function()) and if(function()).
The same but not really:
The ++ and -- operators are actually two operators each, not one operator each. These operators are differentiated by the order they come in compared to their operand. These operators are called the increment and decrement operators. They increase or decrease a value by 1 and generate an output.
The basic process:
a++ or a-- will output first, and increment/decrement second. This is post-increment or post-decrement.
var/a = 4 //the value of a is 4
world << a++ //outputs: "4"
//the value of a is now 5
world << a-- //outputs: "5"
//the value of a is now 4
var/a = 4 //the value of a is 4
world << ++a //outputs: "5"
//the value of a is now 5
world << --a //outputs: "4"
//the value of a is now 4
Pre and post decrement are used very often in loops that access lists, but that doesn't mean that is where their only usage is.
A few iteration variations:
//naive while loop iteration
var/count = 1
while(count<=l.len)
world << l[count]
count += 1
//standard while loop iteration
var/count = 1
while(count<=l.len)
world << l[count++]
//standard in-condition while loop iteration
var/count = 0
while(++count<=l.len)
world << l[count]
//standard for loop iteration
for(var/count=0;count<=l.len;count++)
world << l[count]
//optimized C for loop iteration (optimized by C compilers, but not by DM's)
for(var/count=0;count<=l.len;++count)
world << l[count]
//optimized DM for..in..to iteration
for(var/count in 1 to l.len)
world << l[count]
Another common practice is when you need to check if a function argument has been supplied and if so fall back to a default value to do something like this:
mob/proc
Example(Dir=0,Speed=0)
if(!Dir) Dir = dir
if(!Speed) Speed = step_size
Refire(Dir,Speed)
This is where the || operator comes in. You don't need to navigate troublesome branches to use it, and it doesn't just belong in if statements.
mob/proc
Example(Dir=0,Speed=0)
Refire(Dir||dir,Speed||step_size)
If this is confusing, it's probably because you don't know about something that && and || operators do inherently. It's called short-circuiting.
Short Cirtcuiting:
Short-circuiting is a concept particular to && and || operators. && and || operators check for the boolean condition of two operands. If the left operand meets the condition, the right operand is tested. If the left operand violates the condition, the right operand is abandoned.
We can use this behavior to our advantage if we understand the rules. This isn't about optimization to make your existing code faster. If you already have a project that doesn't take advantage of these concepts, it's usually not worth going back to change working code to use them. But if you understand these concepts, you will inherently write better code in the future. This is about building good habits through improved understanding, not fixing problems after the fact.
if(value1)
return Refire()
else
return 0
We can use short-circuiting to remove the need for a logical branch here:
return value1&&Refire()
The order that we insert operations into boolean expressions is important because more CPU intensive operations should generally be on the right, and less CPU intensive operations should generally be on the left. The above example will prevent the function call from happening if value1 is false. This is often a very useful optimization for preventing function calls where an if statement would otherwise be used. Again, this isn't worth rewriting code for. It's a very, very small optimization, but it is worth understanding so you can adjust your habits in the future to take advantage of conditional logic.
The ternary conditional operator:
The ternary operator is an awful lot like an if statement, but it's inline. It will return one of two values based on a condition. An if statement involves a lot of work that a ternary conditional operator doesn't have to do. Some of this involves scope traversal, which can be costly in the sense of overall performance or may not be possible within the context of the instruction you are writing.
CONDITION ? TRUE : FALSE
The format is simple. The operand to the left of the ? operator is used as a condition, and the operand to the right is the output that is used if the condition is true. This is followed by the ":" separator, the right of which is the false output, which is used when the condition is false.
Understanding what these operators do at a machine level will help you understand how to form better programming habits. They will make you more literate at reading code to boot, which is always a good thing.
Remember that faster code isn't always better code. Sometimes faster code is ugly, unmaintainable, or the speed difference isn't worth the work difference for the programmer. Sometimes worrying about micro-optimization can necessitate the introduction of patterns that are overall less efficient or just plain more cumbersome. Having an understanding of what computers actually do will help you to see when you are using optimal patterns in suboptimal places and will also help you avoid the pitfalls of bad or redundant logic. Programming is often about balancing many factors and making sacrifices in one area over another. There is rarely a perfect solution. Good solutions often require the introduction and elimination of corner cases and requiring the end user to jump through certain hoops. The art of compromise is important, so there is generally not a "best" pattern. Every pattern does a specific task. Getting your head out of the box of just doing what you've seen and injecting true understanding of what operators actually do will help you understand a wider array of patterns. Understanding a wider array of patterns means being able to make better choices every step of the way.
When the compiler runs, it has to transform your code into something that it can process. The gist of this is that it combines all of the code files in #include order and then takes any #defined macro directives and subs out the labels for their values.
When you use preprocessor macros, what you are doing is telling the compiler that a certain label should be replaced with a series of instructions or values. Because this happens at compile-time, definitions can be much faster than variable accesses.
Definitions also have an interesting property of being both global and state-based.
The above will define a new macro label "TILE_WIDTH". Anywhere that you type TILE_WIDTH will be interpreted by the compiler as 32.
The above example would use this preprocessor macro to set the bounds of a mob to be two tiles on each side.
After the expansion phase has done its work, it will look like this to the compiler:
When the compiler reaches an operator that has two constant values, it will attempt to optimize the expression. So it will be interpreted as:
But why would be bend ourselves over backward to use a TILE_WIDTH definition in the first place? The answer is ease of adjustment of large swaths of your source code by changing values around.
You can't use non-constant values when creating prototypes, so you can't do something like this:
The above won't compile because you can't use non-constant values. So the only way to quickly adjust huge chunks of your code by changing a setting or two is to use a preprocessor macro.
Now, there are some questions as to when this would be necessary. Let's say you are making a library that implements a very general solution to a problem. In order for your library to be plug and play, it's good to establish parameters that may vary between uses.
But what if you are just writing a game? Nobody else is going to touch it, so you KNOW that this will never change, so why not use constants?
You don't know that. One day in the future, you might hire an artist that makes a bunch of 16x16 art that better suits your game's style. Now you have all this awesome art, but you've got to wade through thousands of lines of code and change all these little settings. And once you think you've got them all, someone triggers an obscure function buried somewhere in your code and your entire game slips into pixel movement mode because you forgot to change one line. Sounds like fun right?
It's not.