Best Practices
Using Mutty effectively in real-world projects involves understanding how and when to leverage mutable drafts while preserving the benefits of immutability. Here are some best practices and recommendations:
Use Immutable Records as the Default: Design your data models as immutable records and annotate them with
[MutableGeneration]
only when you need mutation capabilities. This ensures your core data remains thread-safe and free from unintended side effects. Mutty will provide mutability on-demand, so you get the safety of immutability by default and only opt into mutation in a controlled manner.Annotate All Nested Record Types: If a record contains properties that are themselves records (or collections of records) which you may need to modify, make sure to annotate those nested record types with
[MutableGeneration]
as well. Mutty can only generate wrappers for types you’ve marked. By annotating the entire object graph of interest, you enable deep mutations through all layers. (If a nested type isn’t annotated, in the mutable wrapper it will remain immutable, meaning you could only replace the whole object rather than mutate its internals.)Mutate in a Local Scope and Finish ASAP: Treat the mutable wrapper (draft) as a temporary tool. Whether you use the
Produce
lambda or theCreateDraft
/FinishDraft
pair, limit the scope of mutations. Perform the needed changes in a tight, focused section of code (e.g., within a single function or a using-block) and then immediately convert back to an immutable record. Avoid passingMutable
objects around or storing them in long-lived variables. This practice prevents accidental sharing of mutable state and keeps your overall design clean and predictable.Convert Back to Immutable Before Exposing Data: Related to the above, never expose a
Mutable...
object to other parts of your application. Always finalize changes withFinishDraft
or by lettingProduce
return the new record, and then pass around the new immutable object. This ensures that outside of the small mutation scope, the rest of your codebase continues to work with immutable data (which is easier to reason about and less error-prone).Leverage Implicit Conversions for Clean Code: Take advantage of the fact that Mutty provides implicit conversions between immutables and mutables. This means you rarely need to call methods to get the conversions – you can assign directly. For example, you can return a
MutableOrder
from a method that expects anOrder
and it will convert automatically. UsingProduce
is also a way to leverage this implicit behavior while keeping your code concise. Embrace these features to write clearer code, but also be mindful that under the hood an object is being created or converted (e.g., don’t accidentally convert in a tight loop unnecessarily).Favor
Produce
for Simple Updates, Drafts for Complex Logic: In many cases, a one-liner withProduce
is sufficient and most expressive. It keeps the mutation inline with the creation of the new immutable result. However, if the update logic gets complicated (multiple conditional changes, loops, etc.), don’t hesitate to useCreateDraft
to break it out. This can make the code more readable than one huge lambda. Just remember to callFinishDraft
at the end. Choosing the right approach will keep your code both clear and efficient.Be Aware of Performance for Large Structures: Mutty is optimized by using incremental generation (so it doesn’t slow down your compile noticeably) and by leveraging efficient .NET immutable collections. However, if you are mutating extremely large object graphs or very large collections, be mindful that Mutty will copy those structures. For example, converting a very large
ImmutableList
to aList
and back has a cost roughly proportional to the size of the list. This is still often cheaper and certainly simpler than manually writing the copy logic, but in performance-critical sections you should measure and understand the overhead. In most applications, this cost is negligible, but it’s good practice to use Mutty for what it’s intended – making necessary mutations easy – and not as an excuse to mutate far more often than you otherwise would.
By following these guidelines, you can integrate Mutty into your project in a way that enhances development productivity and code safety. Mutty’s approach is fundamentally about enabling controlled mutation; as the developer, you still architect your code to use that capability wisely.