Skip to content

Commit

Permalink
selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
bcpeinhardt committed Feb 28, 2024
1 parent 7e2aa87 commit 241f9fd
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 19 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ This resource presumes ALOT of prerequisite knowledge in it's users.
1. You kinda know what Erlang concurrency and OTP is about/why it's good. If you've yet to
drink the Kool-aid, allow me to provide some:
[The Soul of Erlang and Elixir talk](https://www.youtube.com/watch?v=JvBT4XBdoUE)
2. You are familiar w/Gleam syntax. Gleam has a very small set of language features that work well together.
2. You are familiar w/Gleam syntax. Gleam has a very small set of language features that all work well together.
It takes the idea of having only one way to do something pretty seriously, and as such is really quick
to learn. If you have never seen Gleam, try out the [interactive tour](https://tour.gleam.run/).
to learn. If you have never seen Gleam, try out the [interactive tour](https://tour.gleam.run/), or follow
the [Syllabus on Exercism](https://exercism.org/tracks/gleam/concepts).

Each section is broken into it's own module, some sections with submodules. You can read them in the order
you like, but I recommend
1. concurrency_primitives
2. actors
2. tasks
3. actors
4. supervisors

All the code in this project is runnable, just run `gleam run -m <module_name>`. Feel free to clone the repo
and tinker with the code!
Expand Down
1 change: 0 additions & 1 deletion src/actors.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
////

import actors/pantry
import gleam/erlang/process

pub fn main() {
// To demonstrate what actors are and how they work, we'll create a simple
Expand Down
5 changes: 5 additions & 0 deletions src/actors/pantry.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,15 @@ 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.
//
// There's a lot going on with this example, so don't worry if you need to sit
// with it for a while.
61 changes: 60 additions & 1 deletion src/concurrency_primitives.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import gleam/io
import gleam/erlang/process
import gleam/erlang/process.{type Subject}
import gleam/string
import gleam/function
import gleam/int

pub fn main() {
// A "process" in gleam is a lightweight, concurrent unit of execution.
Expand Down Expand Up @@ -62,6 +64,7 @@ pub fn main() {
// and we don't have to worry about the order in which the messages are sent.
let assert Ok("whats up, pluto") = process.receive(subj2, 1000)
let assert Ok("goodbye, mars") = process.receive(subj, 1000)

// Typically, when writing concurrent programs in Gleam, you won't work
// with individual processes a lot. Instead, you'll use higher-level
// constructs.
Expand All @@ -77,4 +80,60 @@ 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.
//
// Before you run off reading those sections though, lets discuss subjects a
// bit more.

let subject: Subject(String) = process.new_subject()

// A subject works a bit like a mailbox. You can send messages
// to it from any process, and receive them from any other process.

process.start(
fn() { process.send(subject, "hello from some rando process") },
True,
)

process.start(
fn() {
// receive in another rando process
let assert Ok("hello from some rando process") =
process.receive(subject, 1000)
},
True,
)

// Notice that the subjects type is `Subject(String)`. Subjects are generic
// over the type of message they can send/receive.
// This is nice because the type system will help us ensure that we're not sending/receiving
// the wrong type of message, and we can do less runtime checking than in a dynamic language.

// The other thing you'll want to know about is "selectors". Remember the example from earlier,
// where we sent messages from two different subjects? We had to choose which ones to wait for
// first (we waited for pluto, then dealt with mars after).
// What if we wanted to deal with messages as they came in, regardless of which subject they came from?
// That's what selectors are for. They let you wait for messages from multiple subjects at once.

// The catch is that selecting from a selector has to produce only one type of message, so you'll
// need to map the messages to a common type.
// In this example, I want to receive messages as strings, so I tell the selector to turn subject 1's
// messages into string using `int.to_string`, and to leave subject 2's messages alone
// using `function.identity`.

// Try sending the messages in different order to see the selector in action!

let subject1: Subject(Int) = process.new_subject()
let subject2: Subject(String) = process.new_subject()
let selector =
process.new_selector()
|> process.selecting(subject1, int.to_string)
|> process.selecting(subject2, function.identity)

process.start(fn() { process.send(subject1, 1) }, True)
process.start(fn() { process.send(subject2, "2") }, True)

let assert Ok(some_str) = process.select(selector, 1000)
io.println("Received: " <> some_str)
let assert Ok(some_str_2) = process.select(selector, 1000)
io.println("Received: " <> some_str_2)
}
37 changes: 23 additions & 14 deletions src/tasks.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import gleam/io
import gleam/otp/task
import gleam/erlang
import gleam/erlang/process
import gleam/list
import gleam/int
Expand All @@ -14,14 +15,13 @@ pub fn main() {
task.async(fn() {
process.sleep(1000)
io.println("Task is done")
Nil
})

// Do some other stuff while the task is running
io.println("I will execute right away")

// When you need the result, block until it's done
let assert Nil = task.await(handle, 1000)
task.await(handle, 1000)

// Now that the task is done, we can do more stuff
io.println("I won't execute until the task is done.")
Expand All @@ -30,40 +30,49 @@ pub fn main() {
// the current process will panic! Most of the time, you don't want that.
// You can use `task.try_await` to handle the timeout gracefully.

let handle =
task.async(fn() {
process.sleep(2000)
Nil
})
let handle = task.async(fn() { process.sleep(2000) })

case task.try_await(handle, 1000) {
Ok(_) -> io.println("Task finished successfully")
// This is the one that will execute
Error(_) -> io.println("Task timed out!")
}

// By default tasks are "linked" to the current process.
// By default task processes are "linked" to the current process.
// This is a bidirectional link:
// If the current process panics and shuts down, the task will too.
// If the task panics and shuts down, the current process will too!.

//
// FOOTGUN ALERT!: `task.try_await` will only protect you from a timeout.
// If the task panics, the current process will panic too!
// If you're thinking "That's kinda shit, what if I want to handle panicked
// processes gracefully?", well, OTP has amazing constructs for that
// called supervisors. It'd rather you use those than roll your own
// shitty version.
//
// If you REALLY want to handle crashing tasks yourself, there's a function
// called [rescue](https://hexdocs.pm/gleam_erlang/gleam/erlang.html#rescue)
// which takes any function and converts a panic into a result.
// It's really meant for handling exceptions thrown in ffi erlang/elixir
// code though, you shouldn't need it when operating from the relative safety
// of Gleam land

let assert Error(_) = erlang.rescue(fn() { panic })

// And an example using it with tasks, shame on you for ignoring my sage advice.
// See how I used `erlang.rescue` on the very inside? That's important.
// Calling `task.await` on a process that panics will generate a timeout,
// which will crash everything anyway.
let assert Error(_) =
task.await(task.async(fn() { erlang.rescue(fn() { panic }) }), 1000)

// To be clear, we're talking about protecting you from weird hard to
// uncover concurrency bugs and network issues. Gleam's static typing
// and immutability will do a good job protecting you from the run of the
// mill index out of bounds/foo is undefined bullshit.
// Just have your task return a `Result`

let handle =
task.async(fn() {
let arr = [1, 2, 3]
list.at(arr, 99)
})
let handle = task.async(fn() { list.at([1, 2, 3], 99) })

case task.await(handle, 1000) {
Ok(val) -> io.println("The 100th item is" <> int.to_string(val))
Expand Down

0 comments on commit 241f9fd

Please sign in to comment.