diff --git a/src/actors/pantry.gleam b/src/actors/pantry.gleam index 3c10d5f..b34b62b 100644 --- a/src/actors/pantry.gleam +++ b/src/actors/pantry.gleam @@ -12,7 +12,7 @@ import gleam/set.{type Set} // We use this to send messages to the actor. We could abstract that away further, // so the user doesn't have to manage the subject themselves, but following this // pattern will help us integrate with something called a `supervisor` later on, and it -// matches how working with normal data in Gleam usually works too `operation(subject, arg1, arg2...)`. +// matches how working with normal data in Gleam usually works too `module.operation(subject, arg1, arg2...)`. // - Functions that need to get a message back from the actor use `actor.call` to send a message // and wait for a reply. (this is just a re-export off `process.call`). It is a synchronous operation, // and it will block the calling process. @@ -38,14 +38,17 @@ pub fn take_item(pantry: Subject(Message), item: String) -> Result(String, Nil) // See that `_`? That's a placeholder for the reply subject. It will be injected for us by `call`. // // If the underscore syntax is confusing, it's called a [function capture](https://tour.gleam.run/functions/function-captures/). - // It's a shorthand for `fn(reply_with) { TakeItem(reply_with, item) }`. + // It's a shorthand for `fn(reply_with) { TakeItem(reply_with, item) }` where `reply_with` is a subject owned by + // the calling process. Two way message passing requires two subjects, one for each process. // // Also, since we need to wait for a response, we pass a timeout as the last argument so we don't get stuck - // waiting forever if our actor gets struck by lightning or something. + // waiting forever if our actor process gets struck by lightning or something. actor.call(pantry, TakeItem(_, item), timeout) } /// Close the pantry. +/// Shutdown functions like this are often written for manual usage and testing purposes. +/// In a real application, you'd probably want to use a `supervisor` to manage the lifecycle of your actors. pub fn close(pantry: Subject(Message)) -> Nil { actor.send(pantry, Shutdown) } @@ -66,7 +69,7 @@ pub type Message { } // This is our actor's message handler. It's a function that takes a message and the current state of the actor, -// and returns a new state and a new state for the actor to continue with. +// and returns a new state for the actor to continue with. // // There's nothing really magic going on under the hood here. An actor is really just a recursive function that // holds state in its arguments, receives a message, possibly does some work or send messages back to other processes, @@ -108,3 +111,11 @@ fn handle_message( } } } +// That's it! We've implemented a simple pantry actor. +// Note: This example is meant to be straightforward. In a really system, +// you probably don't want an actor like this, whose role is to manage a small +// piece of mutable state. +// Utilizing process and actors to bootstrap OOP patterns based on mutable state +// is, well, a bad idea. Remember, all things in moderation. There are times when +// a simple server to hold some mutable state is exactly what you need. But in a +// functional language like Gleam, it shouldn't be your first choice. diff --git a/src/concurrency_primitives.gleam b/src/concurrency_primitives.gleam index 5f76b37..f38046b 100644 --- a/src/concurrency_primitives.gleam +++ b/src/concurrency_primitives.gleam @@ -67,7 +67,7 @@ pub fn main() { // constructs. // // If you want something like a server, a long running process which will - // receive and respond you messages, the "actor" abstraction is the way to + // receive and respond to messages, the "actor" abstraction is the way to // go. (An "actor" is Gleam's equivalent of Erlang/Elixir's `gen_server`. // It has a different name because it has a different API due to static typing, // but it's the same concept.) @@ -77,18 +77,4 @@ pub fn main() { // synchronous code to run concurrently and only block once you need // the results, you'll want the `Task` module. It's great for the dead simple // "do this somewhere else and I'll let you know when I need it" case. - - // A WORD OF WARNING: OTP's abstractions are really powerful, and have been - // tailored over decades so that they fit the mental model of a lot of - // problems really well. It can be tempting to want to use them as organizational - // constructs in the code base, especially coming from an OOP background, as processes can - // hold and update state, but you should not do this. - // Always ask yourself: Do I REALLY need concurrency here? REALLY REALLY? - // If you find yourself reaching for processes/tasks/actors as a way to hold state, - // rather than because you need concurrency for performance/scalability/fault tolerance reasons, - // you are not the first and you won't be the last. - - // Below are some resources for learning functional design patterns to help reduce dependency - // on stateful constructs: - // Todo: find and vet said resources } diff --git a/src/supervisors.gleam b/src/supervisors.gleam index 71b01c6..01de638 100644 --- a/src/supervisors.gleam +++ b/src/supervisors.gleam @@ -2,11 +2,9 @@ //// import gleam/io -import gleam/iterator import gleam/otp/supervisor -import gleam/otp/actor import gleam/erlang/process -import supervisors/a_shit_actor.{type Message} as duckduckgoose +import supervisors/a_shit_actor as duckduckgoose pub fn main() { let parent_subject = process.new_subject() @@ -17,18 +15,25 @@ pub fn main() { |> supervisor.add(game) } + // We start the supervisor let assert Ok(_supervisor_subject) = supervisor.start(children) + + // The actor's init function sent us a subject for us to be able + // to send it messages let assert Ok(game_subject) = process.receive(parent_subject, 1000) // Good messages, nothing crashes - io.debug(duckduckgoose.duck(game_subject)) - io.debug(duckduckgoose.duck(game_subject)) - io.debug(duckduckgoose.duck(game_subject)) - // // Oh shit, that aint good, our actors gonna crash - // io.debug(duckduckgoose.goose(game_subject)) - - // // No worries, supervisor turned things back on for us - // process.sleep(10_000) - // io.debug(duckduckgoose.duck(game_subject)) - // io.debug(duckduckgoose.duck(game_subject)) + let assert Ok("duck") = duckduckgoose.duck(game_subject) + + // Oh shit, that ain't good, our actor is gonna crash + let assert Error(_) = duckduckgoose.goose(game_subject) + + // Don't worry, the supervisor restarted our actor, and the actor's + // init function sent us back a subject owned by the new process. + let assert Ok(new_game_subject) = process.receive(parent_subject, 1000) + let assert Ok("duck") = duckduckgoose.duck(new_game_subject) + + // There will likely be an error report in your terminal, but don't worry + // everythings still working fine. Check it out + io.println("It's all good in the hood baby") }