Introducing: scopes #292
dphfox
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Starting with Fusion 0.3, you may see errors similar to these in your output.
TL;DR - what do I need to do?
Previously, objects could be constructed free-standing, like this:
However, as of Fusion 0.3, objects must now be constructed with a 'scope'. Attempting to construct them as above will emit an error.
If you're inside a
Computed
object or similar, a scope will be passed to you. You can construct new objects using this scope; the objects you make will be destroyed by theComputed
when it's done with them.If you're writing reusable code (for example, a component) then it's usually best to ask for a scope. This means you can pass through scopes from
Computed
objects, for example, which will let you do the same as above.At times, you might not have a scope available to use, for example, in your main script when you've first started up. You can use
scoped(Fusion)
function to make a new one manually. When you're done with it, you must calldoCleanup()
on the scope, to signal that you are done using it.This unified 'scope' system replaces the functionality previously provided by multiple older features, including destructors on state objects, the
[Cleanup]
special key, etc. As such, you no longer need to provide these.What's the problem with the old way?
In previous versions, constructing objects free-standing was not a problem. Since older versions of Fusion depend on Luau's garbage collection process to destroy unused objects, you didn't have to destroy objects yourself - they'd naturally get cleaned up after a while.
For various reasons (including some serious ones involving performance regressions and memory leaks), Fusion now requires all objects to be explicitly
:destroy()
-ed.With no other changes, you would have to write a slew of
:destroy()
calls at the end of every block of code that creates objects in this way. Not only would this be annoying to write out, but it would also be error prone; it would be incredibly easy to leave out one or two objects by accident. For some parts of Fusion (e.g.Observer
disconnect callbacks), you already had to do this, and the UX has historically been subpar.To manage this complexity in existing codebases, developers have often added objects to arrays, so later on you can destroy the whole array at once.
This replaces those harder-to-track
:destroy()
calls withtable.insert
calls that you can keep next to the object construction, so it's easy to see if it's missing and it's easier to copy and paste the object between different parts of the program. It also lets you easily ensure that objects are destroyed in reverse order, so older objects aren't destroyed before newer ones (which can be important if the newer objects were constructed using the older objects).While it's trivially easy to implement in pure Luau, you might also see this presented as a library such as
Maid
orJanitor
:Fusion neatly supports this via the
doCleanup
function, which will call:destroy()
on all the array members. As of 0.3, it is guaranteed to clean up in reverse order, identically to above.While this is a lot better than managing disparate
:destroy()
calls, it still introduces an unacceptable level of verbosity, doesn't flag up warnings or errors if you forget to pair up an object with an array, and doesn't protect you from callingdoCleanup
at the wrong time - or never calling it at all.It also prevents some nice features we enjoy today, such as nesting state objects. You can't put this
Computed
inside of theSpring
definition; instead, you are forced to define it as a separate variable so you cantable.insert
it:First-class support
After observing this behaviour and usage pattern from users, it was deemed worthwhile to allow Fusion to better integrate with these solutions. That's why, starting with 0.3, we're supporting them as a first-class citizen of Fusion's world, under the trendy name: scopes.
Defined formally, scopes are arrays of things to be cleaned up. Outside of some syntax conveniences, that's all they are.
Constructors require scopes
To reduce the number of
table.insert
calls, and to better ensure that every object gets paired up with an array, all of Fusion's constructors (includingNew
) now require a scope as their first argument. Anything created by the constructor is added to the array.In particular, this preserves the old ability to construct objects without saving them to a named variable. Notice how this
Computed
can be nested inside theSpring
definition.Syntax sugar for scopes
Fusion 0.3 also introduces a new function,
scoped()
, which constructs an empty scope for you.The thing that makes
scoped
useful is that{}
parameter. Inside there, you can define constructors that you want to access, and it'll let you access them as a method on the scope itself.In most cases, you'll want access to all of Fusion's constructors. You can pass the
Fusion
table in directly, and it works happily;Besides tidying up the argument list,
scoped()
syntax consistently moves the scope to a unified position that's easy to copy and paste, regardless of what coding conventions you prefer. Here's whatNew
looks like withscoped()
syntax, which would otherwise be awkward to express:By using
scoped()
syntax, your futuristic Fusion code reads and writes almost the same way it does today. Better yet, since you specify the constructors yourself, you can expand it to include your own expansions or your favourite libraries, so you can use it everywhere and with everything.Type definitions for scopes
To support usage of scopes in strictly typed codebases, Fusion 0.3 exports a new
Scope<T>
type, whereT
refers to the table of constructors.If you are using
scoped(Fusion)
, you can setT
to betypeof(Fusion)
. This is particularly useful for writing components that accept scopes as properties:Fusion-provided scopes
In previous versions of Fusion, managing the destruction of dynamically generated stuff took some effort. Often, you'd create an array to store your cleanup tasks, and pass it to a
[Cleanup]
key on an instance that later gets destroyed by a destructor somewhere else, usually on a state object.Fusion 0.3 simplifies this. Instead of having to create and manage scopes inside
Computed
/ForKeys
/ForValues
/ForPairs
, these state objects will do it for you and provide it as a second parameter. When the state object is done with your returned value, the scope is cleaned up for you, destroying anything you've made using it.Constructors are preserved from the outer scope, so you can continue to access any custom constructors you specify with
scoped()
.Shadowing is encouraged, just like
use()
; if you have ascope
variable name outside the state object, feel free to redefine it inside the state object, and enjoy the ease of copying code in and out without having to rewrite a single word.With this change, destructors and the
[Cleanup]
special key are made redundant. If you need custom destruction behaviour, you can insert a function into the scope to run custom code. In fact, you can insert anything into the scope just like you would into[Cleanup]
ordoCleanup()
. Alternatively, if you don't need destruction behaviour, you can ignore it completely.Predictive destruction order warnings / lifetime analysis
With the introduction of scopes, Fusion can do more than just error when you try to use an object that was destroyed in the past. By analysing the scopes, Fusion can now predict the order that objects will be destroyed in, which means it can now warn you ahead of time when you're likely to run into a use-after-destroy error.
Developers who are familiar with Rust programming likely already know the benefits of this kind of analysis. This is an experimental feature; depending on performance characteristics, it might be tweaked, made opt-in, or removed, so your feedback is massively appreciated in shaping this.
Known issues (pre-0.3 release)
These issues should be fixed by the time 0.3 releases.
If you run into any issues, let me know and I can add them to this list.
Inner scope type inference
Luau's type checking often makes mistakes with inner scopes in state objects, for reasons that I haven't yet been able to deduce.
It might be a bug or limitation in the type inference engine, or maybe there's a magic incantation I can write to fix it. I'm not sure, but I'd rather get it out there so you can benefit from it today.
In the meantime, if you're using Luau typing, you should provide a type for the
scope
parameter when you useComputed
,ForKeys
,ForValues
orForPairs
.With the implementation and release of scopes, Fusion is closing the book on a years-long dramatic saga fighting against a bevy of memory- and soundness-related issues. I went into this design process aiming to solve these problems in a way that encourages you towards the correct solution at every stage, while minimally damaging the concise and readable syntax that Fusion has become known for. The ultimate north star was to make the first intuitive code snippet you write, also be the correct and leak-free code snippet. I also wanted to bolster the power of analysis tools to ensure that any possible memory leaks or problems can be detected as far ahead of time as possible; ideally in the development environment, but failing that, as soon as the possibility of a problem is introduced into the system.
I personally believe that this solution that we arrived at together meets those criteria better than I even thought possible. My hope now is that, in your hands, it makes your life easier and lets you sleep at night knowing your Fusion code won't blow up when you're gone.
Let me know if your Tuesday night gets interrupted. I'll hunt down any issues you find.
-Dan
Beta Was this translation helpful? Give feedback.
All reactions