Learning Gleam Concurrency: A TypeScript Developer's Journey
๐ Learning Gleam Concurrency: A TypeScript Developer's Journey
So here I am, diving into Gleam after years of wrestling with async/await in TypeScript, and let me tell you - it's like discovering a completely different way to think about concurrency! ๐ง โจ
If you're like me and have been living in the TypeScript world for a while, Gleam's actor-based concurrency model might feel like learning to drive a manual car after years of automatic. It's different, it's powerful, and honestly? It's pretty darn cool once you get the hang of it.
๐ค The TypeScript Way vs The Gleam Way
In TypeScript, we're all about async/await, Promises, and managing state across async boundaries. It's like juggling - you need to keep track of what's happening where and when.
// TypeScript: The async/await dance
async function fetchUserData(userId: string): Promise {
const user = await fetchUser(userId);
const posts = await fetchUserPosts(userId);
const comments = await fetchUserComments(userId);
return {
...user,
posts,
comments
};
}
// What if one of these fails? ๐
// What if we want to cancel the whole operation?
// What if we need to share state between these calls?
Now, in Gleam, we have actors - these are lightweight processes that can send and receive messages. Think of them as tiny workers that can only communicate by passing messages. No shared state, no race conditions, just pure message passing! ๐ฏ
๐ฏ Example 1: Building a Simple Chat System
Let's build a chat system where users can send messages. In TypeScript, this would involve managing state, handling async operations, and dealing with potential race conditions.
TypeScript Approach
// TypeScript: Managing state and async operations
class ChatRoom {
private messages: Message[] = [];
private users: Set = new Set();
async addMessage(userId: string, content: string): Promise {
// What if another message is being added at the same time?
// What if the user gets disconnected while we're processing?
const message: Message = {
id: crypto.randomUUID(),
userId,
content,
timestamp: Date.now()
};
this.messages.push(message);
await this.broadcastToUsers(message);
}
private async broadcastToUsers(message: Message): Promise {
// Async broadcasting - what if this fails?
const promises = Array.from(this.users).map(userId =>
this.sendToUser(userId, message)
);
await Promise.all(promises);
}
}
See all those potential issues? Race conditions, error handling, state management... it's a lot to think about! ๐
Gleam Approach
// Gleam: Actor-based chat system
import gleam/io
import gleam/result
// Define our message types
pub type ChatMessage {
UserMessage(user_id: String, content: String)
JoinRoom(user_id: String)
LeaveRoom(user_id: String)
}
pub type ChatRoom {
ChatRoom(messages: List(String), users: List(String))
}
// Our chat room actor
pub fn chat_room_loop(state: ChatRoom) -> Nil {
case receive() {
UserMessage(user_id, content) -> {
let message = "User " <> user_id <> ": " <> content
let new_messages = [message, ..state.messages]
let new_state = ChatRoom(new_messages, state.users)
// Broadcast to all users
broadcast_message(message, state.users)
chat_room_loop(new_state)
}
JoinRoom(user_id) -> {
let new_users = [user_id, ..state.users]
let new_state = ChatRoom(state.messages, new_users)
chat_room_loop(new_state)
}
LeaveRoom(user_id) -> {
let new_users = list.filter(state.users, fn(u) { u != user_id })
let new_state = ChatRoom(state.messages, new_users)
chat_room_loop(new_state)
}
}
}
fn broadcast_message(message: String, users: List(String)) -> Nil {
// Each user gets their own actor to handle messages
list.foreach(users, fn(user_id) {
send(user_id, message)
})
}
Look at that! No shared state, no race conditions, just pure message passing. Each actor has its own state, and the only way to communicate is through messages. It's like having a bunch of pen pals who can only communicate by sending letters! ๐ฎ
๐ฎ Example 2: Building a Game Score System
Let's say we're building a multiplayer game where players can score points. In TypeScript, we'd need to worry about concurrent updates to the score.
TypeScript Approach
// TypeScript: Managing concurrent score updates
class GameScore {
private scores: Map = new Map();
private lock = new Mutex(); // Need to prevent race conditions!
async addScore(playerId: string, points: number): Promise {
await this.lock.acquire();
try {
const currentScore = this.scores.get(playerId) || 0;
this.scores.set(playerId, currentScore + points);
} finally {
this.lock.release();
}
}
async getTopPlayers(limit: number): Promise {
await this.lock.acquire();
try {
return Array.from(this.scores.entries())
.map(([playerId, score]) => ({ playerId, score }))
.sort((a, b) => b.score - a.score)
.slice(0, limit);
} finally {
this.lock.release();
}
}
}
Mutexes, locks, try-finally blocks... it's like building a fortress just to update a score! ๐ฐ
Gleam Approach
// Gleam: Actor-based score system
import gleam/list
import gleam/string
pub type ScoreMessage {
AddScore(player_id: String, points: Int)
GetTopPlayers(limit: Int, reply_to: Pid)
GetPlayerScore(player_id: String, reply_to: Pid)
}
pub type ScoreState {
ScoreState(scores: List(#(String, Int)))
}
pub fn score_actor_loop(state: ScoreState) -> Nil {
case receive() {
AddScore(player_id, points) -> {
let new_scores = update_player_score(state.scores, player_id, points)
let new_state = ScoreState(new_scores)
score_actor_loop(new_state)
}
GetTopPlayers(limit, reply_to) -> {
let top_players = get_top_players(state.scores, limit)
send(reply_to, top_players)
score_actor_loop(state)
}
GetPlayerScore(player_id, reply_to) -> {
let player_score = get_player_score(state.scores, player_id)
send(reply_to, player_score)
score_actor_loop(state)
}
}
}
fn update_player_score(scores: List(#(String, Int)), player_id: String, points: Int) -> List(#(String, Int)) {
// Update or add player score
case list.find(scores, fn(score) { score.0 == player_id }) {
Ok((_, current_points)) -> {
let new_score = #(player_id, current_points + points)
list.replace(scores, #(player_id, current_points), new_score)
}
Error(_) -> {
// Player not found, add new score
[#(player_id, points), ..scores]
}
}
}
No locks, no mutexes, no shared state! Each actor manages its own state, and the only way to interact with it is through messages. It's like having a dedicated scorekeeper who only responds to written requests! ๐
๐คฏ The "Aha!" Moments
Learning Gleam's concurrency model has been full of "aha!" moments:
- No more race conditions! Since actors can't share state, there's no way for two processes to mess with the same data at the same time.
- Fault tolerance built-in! If one actor crashes, it doesn't bring down the whole system. Other actors keep running.
- Testing is easier! You can test each actor in isolation by sending it messages and checking the responses.
- No callback hell! No more nested async/await chains or Promise.all() madness.
๐ฏ When to Use What?
After learning both approaches, here's my take:
- Use TypeScript async/await when: You need to integrate with existing JavaScript libraries, you're building simple CRUD applications, or you're working with a team that's more familiar with imperative programming.
- Use Gleam actors when: You're building highly concurrent systems, you need fault tolerance, you're working with real-time data, or you want to avoid the complexity of managing shared state.
๐ The Journey Continues
Learning Gleam's concurrency model has been like discovering a new superpower. It's not that one approach is better than the other - they're just different tools for different jobs. But understanding both has made me a better developer overall.
The actor model in Gleam feels more like how I naturally think about problems: "This thing needs to handle this type of message, and when it gets that message, it should do this." It's more declarative, more predictable, and honestly? More fun to work with! ๐
If you're a TypeScript developer curious about functional programming and concurrency, I'd definitely recommend giving Gleam a try. It might just change how you think about building concurrent systems!
P.S. - The Gleam community is super friendly and helpful. Don't be afraid to ask questions! ๐