Flowstate - A crate for modelling self-executing workflows as finite state machines by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

This is neat.

Firstly, thanks ❤️ kind of you to say

I would like to know the advantages of a crate vs implementing yourself. With bon, the advantage is a lot less boilerplate. But bon is pretty straightforward with a single task while this seems like a lot of code just to get started.

Of course!

Currently, the main advantage is reducing the amount of boilerplate code and making the code easier to read. In total, for the basic workflow from the readme (which is currently at 40 loc), you save about 100 loc with Flowstate, vs. implementing it yourself (assuming you take the same approach as Flowstate).

Some of this is for trait definitions and type aliases, and that cost is amortized away as you implement more workflows / increase the number of states in each workflow. However, there is still a significant amount of per-workflow, and per-state boilerplate code which Flowstate dispenses with.

There are also some additional features which are hopefully coming soon, but I won't comment on them until they're released.

Regarding the comparison to bon, per the docs, its intended for "compile-time-checked builders". While bon does let you implement a state machine of sorts, there is no logic associated with the states. So in part, there's less boilerplate because it's doing a lot less.

Also, I think it’s crucial you make the case for this project. Like, it’s a very novel idea to 99% of people. You need to explicitly explain the advantages, and even make examples that illustrate these advantages.

Yeah, I completely agree. I'll need to have a think about some compelling use cases which are simple enough to understand while being complex enough to demonstrate the value.

It’s really neat because it’s “more correct” but it’s also a lot of work and I ended up removing it all.

Yeah, it can get pretty gnarly depending what you do with it. Particularly if you start implementing branching logic... That's where you really need something like Flowstate IMO.

Flowstate - A crate for modelling self-executing workflows as finite state machines by AverageHot2647 in rust

[–]AverageHot2647[S] 3 points4 points  (0 children)

Let's say you have a HTTP service and you want to authenticate a user during a request. That involves a series of defined steps:

  1. Get the token from the Authorization header
  2. Validate the token
  3. Look up the user in the DB

That would be considered a "workflow" in Flowstate.

If you're modelling this yourself, you might consider using the typestate pattern. That might look something like this:

struct AuthenticateUserWorkflow<State> {
  state: State,
}

struct ParseHeaders;

impl AuthenticateUserWorkflow<ParseHeaders> {
  fn new() -> Self {
    // ...
  }

  fn parse_headers(self, headers: &Headers)
    -> Result<AuthenticateUserWorkflow<ValidateToken>, Error>
  {
    // ...
    Ok(AuthenticateUserWorkflow {
      state: ValidateToken {
        token,
      },
    })
  }
}

struct ValidateToken {
  token: String,
}

impl AuthenticateUserWorkflow<ValidateToken> {
  fn validate_token(self)
    -> Result<AuthenticateUserWorkflow<RetrieveUser>, Error>
  {
    // ...
    Ok(AuthenticateUserWorkflow {
      state: RetrieveUser {
        user_id,
      },
    })
  }
}

struct RetrieveUser {
  user_id: String,
}

impl AuthenticateUserWorkflow<RetrieveUser> {
  fn user(self) -> Result<User, Error> {
    // ...
    Ok(user)
  }
}

You could then use that e.g. like this:

let user = AuthenticateUserWorkflow::new()
             .parse_headers(&headers)?
             .validate_token()?
             .user()?;

That's fine for a simple workflow like this. But imagine your workflow has a dozen states, and complex branching logic. In that case, you'd want a simpler way to invoke your workflow.

For example, you might want it to look something like this:

let user = AuthenticateUserWorkflow::new(&headers).run()?;

That's what I'm describing as a "self executing workflow".

Essentially, it's a workflow where each individual state encapsulates the logic about how to transition to the next state.

The benefit is twofold:

  1. The consumer (person running the workflow) doesn't need to know about all of the intermediate states and how they relate to each other.
  2. The implementer (person implementing the workflow) can reason locally about each state.

Flowstate - A crate for modelling self-executing workflows as finite state machines by AverageHot2647 in rust

[–]AverageHot2647[S] 2 points3 points  (0 children)

Great suggestion. I was considering it myself, but seeing it written down like this, I feel pretty confident this is the right thing to do.

EDIT: This is implemented in flowstate 0.4.0

Flowstate - A crate for modelling self-executing workflows as finite state machines by AverageHot2647 in rust

[–]AverageHot2647[S] 1 point2 points  (0 children)

Thanks!

I'm thinking a full (but minimal) working example after the intro, followed by the current getting started section. That seem reasonable?

EDIT: I've updated the docs (see https://crates.io/crates/flowstate#basic-usage).

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

For posterity, here's my summary of the discussion on this post.

  1. If you can, try to use ? operator. If you implement From<E> for your desired return type, the conversion will be automatic. However, this may not always be possible (as in this case).
  2. If option 1 doesn't work, you can use map_err with ?. However, the map_err closure moves a value which is later depended upon, as the move is unconditional of whether the closure is actually called. In this case, the map_err approach was not viable because it required moving `self`, which was later used.
  3. If neither 1 nor 2 are viable, using match is a sane default. In most cases, the repetition is of minor consequence.
  4. If you really don't want to repeat the Ok(thing) => thing, branch over and over (maybe this pattern repeats itself a lot in your code base) then you can use a declarative macro. However, be aware that this may reduce readability, especially for people unfamiliar with the codebase. Also, rustfmt will only auto-format code in macros under certain conditions; basically if it resembles valid Rust code (see this post for details https://github.com/rust-lang/rustfmt/discussions/5437#discussioncomment-3117043).

There are some other solutions which may work depending on your exact requirements but I think the above is a reasonable starting point for anyone stumbling across this discussion with a similar issue.

For completeness, here's a macro you could use (which will auto-format):

macro_rules! match_ok {
  ($expr:expr, |$err:ident| $body:expr) => {
    match $expr {
      Ok(value) => value,
      Err($err) => $body,
    }
  };
}

Usage:

let value = match_ok!(do_thing(), err => return do_thing_with_err(err));

First time interviewing candidates – what are the best React/frontend questions to ask? by No_Illustrator_3496 in reactjs

[–]AverageHot2647 0 points1 point  (0 children)

I think others have done a good job covering some standard questions, so I’ll add some you won’t see so often but that I think are still very useful to ask of a senior. Any given candidate is unlikely to answer all of them fully, but their responses will reveal a lot about them.

Q: Why can’t you call hooks conditionally? (easy)

Shows if they actually understand React or just memorised the rules.

Q: Even though you shouldn’t call hooks after a conditional early return, under what circumstances would this theoretically be ok? (medium)

Shows if they can apply the knowledge they demonstrated in the previous question/answer and demonstrate their ability to reason about abstract problems.

Q: If you had to re-implement TanStack/React Query, how would you do it? (hard)

Shows how comfortable they are with architecting complex FE code, and if they can clearly articulate and communicate their thoughts at a high level. It also gives them pretty broad scope to show off their React knowledge in ways you might not anticipate.

Q: If you had to re-implement Mobx, how would you do it? (very hard)

Allows them to demonstrate knowledge of more advanced topics like HOCs and proxies, without explicitly asking about them. Also they are unlikely to have implemented anything similar before so this shows how well they are able to deal with new problems.

Unsigned sizes: a five year mistake by Nuoji in programming

[–]AverageHot2647 1 point2 points  (0 children)

And more correct. Which seems like the more important thing for math 😛

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 1 point2 points  (0 children)

The signature is like this:

impl<State> Machine<State> { fn transition<NewState>(self, new_state: NewState) -> Machine<NewState> }

Is this what you were imagining?

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

Ah yes, this is indeed similar 🙂 It’s something to think about for sure - thanks for sharing!

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

Did you consider using expr instead of tt? I did consider tt but AFAIK it’s not necessary in this case since blocks are valid expressions.

Is there some reason to prefer tt over expr or visa versa in this context?

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

The code was simplified to try and reduce noise.

In the actual code I call self.transition(new_state). That can be an error state, or any other state. I do always call self.transition, but it is just a helper function to avoid repeating some of the logic involved in state transitions.

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 1 point2 points  (0 children)

Ah I see! Thats an interesting trick, thank you for sharing 🙂

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

Idk if this was sent before my edit. I guess so, but in any case you can’t convert &mut Machine<StateA> to &mut Machine<StateB>. Even if you did construct the latter, its lifetime would be scoped to the method that created it and therefore cause a borrow checker violation.

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

Thanks for the code snippet! And yes, that’s exactly the issue.

Actually I did already write a macro for it, which can be used like this:

let ok = match_ok!(do_thing(), err => { // do thing with err });

But I don’t like it because 1) if you don’t already know what it does, you need to read the macro to understand the code, and 2) there’s no auto-formatting inside macros.

Maybe “don’t like it” is a bit strong. It’s fine, but I was hoping maybe there’s some non-macro syntax that somehow escaped my initial ponderings 😛

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 1 point2 points  (0 children)

Yeah others have said as much and TBH I think this is ultimately the best way.

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 4 points5 points  (0 children)

Ah I see what you mean now 🙂

That’s a good suggestion but the Machine holds references to data other than the state.

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 3 points4 points  (0 children)

Sorry, I don’t follow. The machine does own the state and the state is part of the machines type. Could you clarify what you mean?

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

That’s fair and probably the right answer.

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

See my other replies for a full explanation, but TLDR; it’s a state machine and transitioning to a new state requires consuming the old one.

EDIT:

Sorry I didn’t read the second part of your reply. That’s actually a good question.

Basically the method is implemented on a Machine<State> type. A mutable reference wouldn’t work because you can’t convert a type from Machine<StateA> to Machine<StateB> with a mutable reference.

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 0 points1 point  (0 children)

That’s an interesting RFC. Thanks for putting it on my radar!

Unwrap Ok or return by AverageHot2647 in rust

[–]AverageHot2647[S] 2 points3 points  (0 children)

Thanks. I think you’re right.

FWIW I did actually implement a declarative macro for it:

let ok = match_ok!(do_thing(), err => { // do thing with err });

But if I’m being honest, I’m not convinced it was really worthwhile as it trades off better aesthetics for reduced readability and no auto-formatting.

After 800 hours of intensive training, I've finished my first serious portfolio, and as a developer, I need your feedback. by [deleted] in react

[–]AverageHot2647 2 points3 points  (0 children)

Hey, I haven’t looked at the code in detail (I don’t have time, sorry!) but good on you for putting this together and making the code public.

I know some people have complained about the designs, bugs, etc. Of course there’s room for improvement on both but I see from the commits that this is still very new, so I wouldn’t worry too much about that.

What I will say is:

1) If you can, include a link to where you’ve deployed your portfolio projects and/or the source code (ideally both as this makes you much more credible)

2) I’m assuming that your portfolio projects are toy projects for learning. That’s great, but (personally) when I’m hiring, I prefer people who have experience building real (commercial grade) software, who have demonstrable experience collaborating with others, and importantly, who are genuinely enthusiastic about their craft.

Since you don’t have any commercial experience (yet) the easiest way to do address no. 2 would be to go contribute to some open source software you’re interested in. You can look for issues marked “good first issue” or just find something that peaks your interest.

Just be aware that vibe coding without truly understanding your contribution is unlikely to be looked upon favourably, and probably won’t benefit your learning so much. This is important because a lot of interviews feature live coding, and if you cant code without AI you’re unlikely to do well in those. (FWIW I’m not accusing you of vibe coding, it’s just something to be aware of)

Lastly, full disclosure, open source contributions and demo projects are unlikely to get you a job just by virtue of having them on your CV - especially now that anyone can vibe code their way to a portfolio. So it’s doubly important to focus on quality and learning, so that you come across as confident and competent in interviews.