This article shows how to use some language features of F# to clean up the code from part 2.
In this part of the series we'll have a look at the F# script from the the previous part and see how we can use some language features of F# to improve the readability.
If you want to test the code you can download the project files.
Let's have a look at the code in its current state.
Input and movement
The changes I made between the C# and the F# version
shortened the script and removed some code duplication in lines 18 to 23.
I could have done a similar thing in C#, but since that would have meant adding a new method to the class it would not have seemed worth the effort. After all it would have meant that someone reading the code would have had to jump back and forth in the code of the Player class.
F# on the other hand allows us to declare functions within functions. That way the declaration of the check-function is right next to the line where it is used, which improves the readability. The other effect of writing the function inside the _Process method is that now it's abundandly clear that the check function will never be used except inside the _Process method, which helps clear up the clutter even more.
But now we have split the code into the check function and the code that calculates the movement vector, and while the way the movement vector is calculated is somewhat project specific (since it includes the names of the actions to be checked) the check function really isn't. It's just converting an action to a factor which is quite useful, but it's in no way something that a developer would have to read in order to understand what the Player class is doing.
Besides it does not access any members of the Player class, so it's neither helpful nor necessary that the code is in here instead of somewhere else.
So let's move it. If you are coming from an object oriented language your first idea would probably be to turn it into a static class function (just like the IsActionPressed()-function is a static function inside the Input class). F# has a very concise syntax for doing exactly that.
Let's add a new module directly above the PlayerFs class for now.
A module allows us to declare functions outside of classes. Well, technically modules are compiled into classes with static functions, so effectively we are doing the same thing we would have done in C#, but the concise syntax lowers the threshold considerably.
Game code to library code
Let's follow that train of thought even further - the way the movement vector is created is not very special. Other games will certainly use different action identifiers, but using two for left and right and two for up and down is not exactly groundbreaking news.
Since we already have a function that turns one action into 1 or 0 let's write another one that turns two actions
into -1, 0 or 1.
Using that new function we can then declare a function that turns four actions into a Vector2. Just by writing "module GdInput =" and moving a couple of lines we have turned code that was very specific to our Player class into code that could be reused everywhere in the project.
That also affects the way the functions should be written. The name "check" was fine for a local function - it's short and sufficient as a mnemonic, but it's really not self-explanatory. So let's rename "check" to "actionToFactor", "checkTwo" to "actionsToFactor" and "checkFour" to "actionsToDirection".
Another thing that should be changed between game code and library code is how we deal with types. Game code inside the Player class should be as concise as possible so that you can understand what the code is supposed to
do at a glance. Type information is not really important at that point, since we don't really care what kind of
vector is returned by "actionsToDirection" and then passed to "this.Translate". If the returned type doesn't match
the expected type the compiler will complain, and since both lines are directly next to each other it will be obvious
what it's complaining about.
The functions that were moved to the GdInput module don't need to be as concise anymore, since no one really needs to read the whole GdInput module and understand it at once. People are going to look at the GdInput module once the results they get from one of its functions doesn't match what they expected, so the type information in the module should be unambiguous.
Like most languages with type inference F# allows us to specify the data type of a variable or parameter by putting a ": type" behind it.
Finalizing the module
Last but not least let's add some documentation to the functions so that when we use the function someplace else we get a helpful tooltip.
Notice how the change in coding style affects the way we will deal with the code in the future: the need for more explicit naming, the explicit typing and the comments add a significant amount of work to every code change, which is why we didn't apply this style to the code in the Player class, since game specific code like that should be easy to modify so you can quickly prototype new ideas without risking outdated comments or constantly running into compiler errors because you forgot to adjust a data type.
Library code (i.e. code that is meant to be reused) does not need to be modified as easily, in fact changing it is risky, since there's a good chance that changing it might break one of the many places where it was used.
That's also why comments are so important with this type of code, since a programmer that uses a library function should rely on the intended functionality, not the factual functionality. If our function doesn't do what it is supposed to do and someone intentionally used that broken behaviour, then fixing our function would break that code rather than fix it.
So as a final step let's move the module code into a file of its own. Careful: when you create a new .fs file make sure that it appears above the Library1.fs file since F# files are evaluated in the order they appear in the project's file list.
Player code to game code
Now the functions that generate the input direction are moved out of the Player class, but the call to actionsToDirection is still there and it lists all of the actions used to control the player.
Again this is something that is not really unique to the Player class. If we wanted to have a menu react to the input we would have to repeat the same line again and get the usual risks and downsides of code duplication. So let's move that code out of the Player class as well, but since that code is game specific let's keep it insie the file.
Now let's take care of the animation related code. The code in here should be self-explanatory.
Node extension method
And finally let's move the code that fills the anim node. Since that code needs a reference to "this" we can't just move
it into a module unless we want to change the signature so that this is passed as a parameter.
Luckily F# does have a feature that allows us to add methods to existing classes: extension methods (as known from C#).
In order to add a method to an existing class we start with
type ABC with
and then list public members just like we would in the class declaration.
Generics with constraints
Since we want the method to return a typed node (in this case an AnimationPlayer) we need to use generics, but since
the result of GetNode can never be anything that doesn't inherit from Node we need to add a constraint to the type parameter.
That's why getNode has a type parameter of "'a when 'a :> Node" which reads as "Some type called 'a that fulfills the constraint that 'a must inherit from Node.".
Library code vs. game code
Note again how the library code in the extension method is much more verbose, since that code provides a lot of information about the involved types whereas the game code in the Player class that calls the new getNode method can now completely rely on type inference resulting in beautifully simple code.
By separating game code from library code we were able to shorten the Player class down from 25 lines to 12. More importantly now the Player class contains almost no line that doesn't contain information that is specific to this Player class, which means that you won't have to search through generic code if you want to understand what the class does or if you want to find the place that needs to be edited to test a different mechanic or a new balancing formula.