F# and Godot part 2
part 1 I went over on why I chose F# and how to set it up with godot.
part 2 I will go over how you do some basic godot things in F# like emitting custom signals.
Part 3 will go deeper in F# and use it to make a library from Godot easier to use.
Setting a value through this._Ready
F# is a functional style programming language at its core. Null is not seen as idiomatic and even worse F# will try to fight you if you make a property that does not start with some kind of value.
Luckily, F# also comes with a solution. And, if you ask me its a lot better than this._Ready
.
So, here is a bit of code taken from my card game that illustrates how to get a child Node and store it for later use.
type LoginScreenFs() as this =
inherit Control()
let userNameNode =
lazy (this.GetNode<LineEdit>(new NodePath("UserName")))
Ok, its weird so lets break it down:
type LoginScreenFs() as this =
this simply declares the type we want to create. The as this
parts makes this
available for the let
bindings. inherit Control()
is used to tell F# what to extend. In this case, we extend the Control node.
Now comes the fun part
let userNameNode =
lazy (this.GetNode<LineEdit>(new NodePath("UserName")))
let userNameNode
simply makes a private field. In this case with the value lazy (this.GetNode<LineEdit>(new NodePath("UserName")))
. We don’t have to set the type for userNameNode
as F# is able to infer it for us to a Lazy<LineEdit>
.
As for how this works, well easy: lazy()
takes a single expression and runs it the first time a value is needed. After that, it remembers the value so the next time the value is needed it can simply return the stored value instead of having to run the code again.
So, in other words instead of setting the value of userNameNode
in this._Ready
we don’t yet give it a useable value and the first time we need a value we search for the node and remember it for next time. Except that we do not have to check ourself, everything is already handled by userNameNode.Value
.
As for why it is better? Simple: Its impossible to forget to give a value to our fields in the constructor
nor in this._Ready
. The less there is to forget, the less bugs there are that you can make. The less bugs you can make, the better.
If you really do need a field that starts without a value, use an option instead.
type LoginScreenFs() as this =
inherit Control()
let mutable userNameNode : Option<LineEdit>= None
override this._Ready () =
userNameNode <- Some(this.GetNode<LineEdit>(new NodePath("UserName")))
Keep in mind that when using this its more code as F# forces you to handle the None (Null) case.
Async anything
There are many differences between async in C# and F#. Lets first have some code examples and then break it down.
A basic async function in C#
public async Task<int> AnAsyncFunction(int a, int b)
{
var newVal = await DoStuffWithAnInt(a);
return a + b;
}
the same function in F#
let AnAsyncFunction (a:int) (b:int) : Async<int> =
async {
let! newVal = DoStuffWithAnInt a
return a + b
}
As you can see, everything is different. Which is kind of the theme when switching to F# from C#. Lets break it down:
The first thing you notice is that F# does not have the concept of an async function
, at least not in the same was as C# does. Instead it makes use of an async block
.
Next, you see that F# doesn’t use await
and if you payed good attention you may also see the !
after the let
in the block look at let! newVal = DoStuffWithAnInt a
.
This !
is very important and tells F# that you don’t want the literal value of the function (in this case Async<int>
) but only care about the inner value (In this case an int
). In other words, it works the same as await
in C#.
There is a bit more going on behind the scenes, as F# allows you to write similar blocks for other types. Search for Computation Expressions
if you want to know a bit more on how it works. In part 3 we are going to use one to make some syntax just a bit nicer. For now, all you need to know is that let!
does what you want await
to do in C#.
Another difference: F# uses its own type called Async<T>
instead of using Task<T>
. You can still use Tasks
in F# though. Just need to convert them using Async.AwaitTask
like so `
let! a = parameter1 |> someTaskFunction |> Async.AwaitTask
There are also libraries like Ply that give you a task
block. This allows you to write the same code as if it was an async
block, but works with Task
instead.
A final difference you have to keep in mind: In F# you have to start an async process yourself, use Async.Start
or one of the many other functions to start them. They all behave a bit differently but are all pretty well documented.
The elephant in the room: Awaiting on signals.
As already stated earlier, using ToSignal()
. In C# you can just use await
and call it a day. In F# this is sadly enough not the case. This is because of the differences in how F# and C# do async amplified by godot doing it just a bit different again than the standard C# way.
Godot does not use a Task
for ToSignal
so, using Async.AwaitTask
does not help us. If you want to use ToSignal
you will have to use ply
and its task block. Additionally, for some weird reason the classes Godot uses are not compatible with ply
despite implementing everything. So you will also need to wrap the output of ToSignal
into your own class to make it compatible.
This is wrapper code that I would have used if I didn’t gave up on Async all together because of the issues with WASM.
open System.Runtime.CompilerServices
open Godot
type SignalAwaiter2(wrapped: SignalAwaiter) =
interface ICriticalNotifyCompletion with
member this.OnCompleted(cont) = wrapped.OnCompleted(cont)
member this.UnsafeOnCompleted(cont) = wrapped.OnCompleted(cont)
member this.IsCompleted = wrapped.IsCompleted
member this.GetResult() = wrapped.GetResult()
member this.GetAwaiter() = this
Using it is simple (though admittedly, not ideal):
let someFunc =
task {
let! x = SignalAwaiter2(this.ToSignal(this.GetTree(), "idle_frame"))
}
You could write an extension method to automatically wrap this.ToSignal into a SignalAwaiter, but I stopped using this system all together before I got so far.
Emitting and listening to (custom) Signals.
Not much has changed with this compared to C#. There are 2 things you need to think about though.
The first is that Godot wants to marshal
the values passed through Signals. This severely limits what you can send through them. Any F# specific type of type like struct records will NOT work. F# specific stuff like Option/Result will as far as I know also NOT work. Use classes that extend Godot.Object
if you want to send them over Signals, just like you would with C# code.
Also, there is no way to define a sub type in a class in F#. As a result, I have yet to find a way to define custom signals in F#. HOWEVER it has an easy work around: Just define them in the C# file you already need anyway. As the F# code is loaded as a library you have access to any F# type you want to send over. It does mean you can’t use nameof
in F# to get the name of the signal when emitting it though.
Other than those 2 things, it works exactly the same. If however these restrictions are a no go however, then both F# and C# have events
which can fill the same role. You just have to remember to manually unsubscribe from these events
whenever a node gets removed.
Next part
The next part will dive deeper into F# and some of it features like discriminated unions
, match
and Computation Expressions
. These will be used to make a wrapper around one of Godot’s libraries to make it A LOT nicer to work with.