Posting with Metals
In the Rust book
, there is a chapter
on Implementing an
Object-Oriented Design Pattern which describes a stateful Posts API
at some length. We will revisit this example in the context of metals
.
Note: The primary objective of the original example and code used in the
Rust book
is educational, not building production ready code. This article should be read in the same spirit and not as a comparison of design decisions or superiority of technical choices.
Original Post Example from Rust Book
The book discusses the State pattern and how to go about implementing it
in Rust
. The crux of the pattern is that the behavior of a value can
change based on a set of states which we track internally.
The book first implements this pattern closely following the object-oriented way first and then refines it using the Rust language capabilities. We will look at both approaches here.
Overview of functionality
Here is a summary of the functionality from the book for each reference:
- A blog post starts as an empty draft.
- When the draft is done, a review of the post is requested.
- When the post is approved, it gets published.
- Only published blog posts return content to print, so unapproved posts can’t accidentally be published.
Object oriented implementation
The code below shows the intended use of this API (reproduced from original article). This will not run yet.
fn main() {
use blog::Post;
// Create new post
let mut post = Post::new();
// Add text to the post
post.add_text("I ate a salad for lunch today");
// Since this post is still in draft and not yet approved, you will
// not see the content of the post yet.
assert_eq!("", post.content());
// Request review of the post
post.request_review();
// Content is still not visible
assert_eq!("", post.content());
// Approve the post
post.approve();
// Now you have access to the content of the post
assert_eq!("I ate a salad for lunch today", post.content());
}
Here is the module that implements the Post functionality as expected
by the text and code above. Again, this is the same code from original
book chapter
, with a lot of added comments to make it easier to follow
for those less familar with Rust syntax and also to bring out some of
our observations.
Now you can run the code from first block using our Post API and see that it is working as intended.
Analysis of object-oriented Post implementation
The original book chapter
goes through a lot of details on the design
and implementation choices that were made as well as their pros and cons.
We will discuss some of them here and then add a few of our own observations.
We highly encourage you to read the original book chapter
. We hope
the discussion shall help everyone, at the very minimum, appreciate the gamut
of design thinking and choices involved in even simplest of API's.
Modeling considerations that we have been discussing in the context of
metals
were not the focus of the authors of the Rust book
in the
chapter
. So, the points we are brining out here are not those of
omission or inadequate design considerations, but more a matter of content
choice on a specific subject.
- From a model perspective (please keep our comments from previous paragraph
in mind), there are a lot of technical details that can be intimidating
even for technical folks without fair understanding of Rust. What the
hell is
Option<Box<dyn State>
? - While
enum
variant is a good choice to represent mutually exclusive states, the choice (oftrait object
) made in the code provides better ergonomics at the API call site (by doing away with the need formatch
expressions). - There is clear separation of concerns.
Post
knows nothing about various behaviors. It also promotes modularity and clarity. There is one place to look at how the system behaves in a given state. - The
Rust book
chapter
has some suggested extensions and hints to implement them. It is seems easy to extend the system with new behaviors. Well, to an extend! - There is strong coupling between states as they implement the transition
to next state.
Using the example from the original bookchapter
, if we add another state betweenPendingReview
andPublished
, such asScheduled
, we would have to change the code inPendingReview
to transition toScheduled
instead. - The transitions are pretty much embedded in code for different states.
We have to go through the code for each
State
to understand the real state transition model of the system. - There is duplication of code where we have to implement all state actions on every state even when they are no relevant transitions.
- We also have to duplicate state actions such as
request_review
andapprove
onPost
which simply delegates to their counterparts inState
. - We definitely don't make illegal states unrepresentable. We are returning empty string for content in states where content access is illegal.
- Externally extending this API can be tricky. In technical jargon, this
implementation suffers from an expression problem.
Adding a new behavior would need us to reach intoPost
implementation code to add new things. Imagine adding reject action or reset_text functionality.
As it is implemented here, we would need to reach intoPost
implementation code to add them. - Referring back to our discussions on modeling data,
this model represents an application programming view.
The primary purpose of Posts is to manage its content. We can think of many ways in which we want to allow the post content to be manipulated - reset the content, change parts of the content, maintain change history, add review comments, so on and so forth.
We will end up adding a lot of code all across states! - This pattern does not address many data modeling concerns. For example,
the
request_review
cause a transition toPendingReview
only if the content is nonempty.
To be fair, that is not what this pattern was designed for. - All states operate on the same globally shared data instead of them operating on just the parts that they are interested in.
- As in most design patterns from the past decade, this is emblematic of top down, system breakdown approach to building modular systems as opposed to compositional approach to building larger system from smaller ones.
Doing it the Rusty way
The original book chapter
also shows how to implement this pattern
differently that is more idiomatic of Rust. This is based on the idea
of implementing states and behaviors as types.
Here is the revised Post code.
We have to make small adjustments to the call site (the main function), but will retain most of it.
Analysis of Rusty Post implementation
Let us take a closer look at the this implementation.
- It makes illegal states unrepresentable. We can only change Post content while it is a Draft and can only access the content once it is approved. This is a small step for API provider, big step of API consumer productivity.
- We seems to have simplified the interface a lot. Lot of scary looking
things like
Box<dyn ...>
have disappeared from the interface. - We have removed a lot of duplicated code and unnecessary implementations.
A lot of good things have happened and looks like this is moving in the right direction. Still there are several things we have not addressed yet.
- The transitions remain embedded in the code.
- There is still very strong coupling between the states. Since these
are types, not the interfaces (those
dyn State
thingies), we probably have made the problem worse. There is tremendous power in our ability to adhere to an interface contract while changing implementations. This is why build APIs in the first place. - This also impact the extensibility of the API both from inside as well
as outside. Ideally, we would like to be able extend the interface of
a state, say
PendingReview
, and provide an implementation for new interface without having to modify the existing code. We need some kind of dependency injection to reduce coupling of states and retain the power of interfaces we had in our original object-oriented implementation. - We really have not addressed any of the data modeling concerns we discussed in our previous analysis section.
- On the other hand, we have completely localized the states and lost the complete idea of a shared data. May be we swung too far. We still want the concept of shared data model and data access so that we can deal with those aspects, say syncing with a datastore more efficiently and effectively.
- Just our behaviors are intricately connected to states, these state we use
to encode different modes of the API are linked to the underlying data.
This means the state actions can fail or transition to different states
depending on the data and the rules governing them.
We are looking at a non-deterministic automaton as opposed to a
deterministic one.
As far as state design patterns go, there are established approaches such as immutable states and fine-grained action interfaces to make it deterministic and keep it simple. All the other concerns are packed up as side effects and handled through a set of other patterns.
At the very least, we should be able to see that the complex business rules are moving away from the neat interfaces we built. The point we are making here is that in an API design, the state/interface dependency is not at the top layer, but they go all the way down. We need to be able to package them and compose them both vertically and horizontally. That sentence may not mean much now, we will return to it when discussmetals
.
On Handling States
Here in this chapter, we are specifically looking at the design pattern
as discussed in the Rust book
. It is important to note that there are
several approaches to handling states besides this single pattern,
especially if you have familiarity with frontend frameworks. If you
have worked in that area, you would have come across Redux
, XState
,
MobX
and many more and their integrations to frontend frameworks
like React
. Redux
, arguably the most popular of them all, has been
ported to many languages including a Rust (redux-rs
).
Redux
uses a more functional pattern to state management. In order
to make the usage simpler and reduce the level of broiler plate code,
there are also several libraries and tools available including
Redux Toolkit
.
There is also a rich field of formal techniques for modeling states,
deterministic and nondeterministic finite automatons (DFA and NFA),
Moore and Mealy machines, Harel statecharts and more (See State diagrams
for a preview).
Another area that has profound insights and imputs to this world is coming
from Dynamial systems theory
. We derive our inspirations heavily from
the work of David I. Spivak
, especially his work on
Polynomial functors
.
Quote to Use.
Here is a quote from the sipc book
(The structure and interpretation of
computer programs).
One powerful design strategy, which is particularly appropriate to the
construction of programs for modeling physical systems, is to base the
structure of our programs on the structure of the system being modeled.
Metal plating the posts
THIS SECTION IS STILL WORK IN PROGRESS
Copyright 2022 Weavers @ Eternal Loom. All rights reserved.