This example demonstrates how to replace mutable functions in Python with additional arguments in Gleam, either using explicit state handling or state passing.
Mutable Dice¶
Hog is a dice game in which players alternate turns. On each turn, the current player rolls up to 10 dice (their choice) and scores the sum of the dice outcomes unless any dice come up 1, in which case they score only 1. The first player to score 100 total points wins.
def roll_dice(n, dice):
outcomes = [dice() for _ in range(n)]
if 1 in outcomes:
return 1
else:
return sum(outcomes)One way to test this function is to provide a deterministic but random-looking
mutable dice function that cycles through outcomes. Cubing a large number (at
least 17) modulo 1013 (a prime number) scrambles the outcomes a bit.
def make_test_dice():
state = [0]
def dice():
n = state[0] + 17
outcome = n * n * n % 1013 % 6 + 1
state[0] += 1 # Update the state for the next call to dice()
return outcome
return diceEach call to make_test_dice creates a function with a fixed sequence of dice
outcomes.
>>> d = make_test_dice() # Create one random-looking sequence of outcomes
>>> [d() for _ in range(10)]
[4, 6, 2, 4, 1, 3, 6, 2, 5, 2]
>>> [d() for _ in range(10)] # Continuing to call d() gives different outcomes
[5, 2, 6, 3, 1, 5, 3, 1, 6, 5]
>>> e = make_test_dice() # But a new dice function has the original outcomes
>>> [e() for _ in range(10)]
[4, 6, 2, 4, 1, 3, 6, 2, 5, 2]And these dice can be rolled 5 times to simulate turns in the game of Hog.
>>> roll_dice(5, d)
13
>>> roll_dice(5, d)
15
>>> roll_dice(5, d)
20Gleam does not support list mutation, and so the way to create the same
effect is to pass in and return the state of the dice.
/// Six-sided test dice whose outcomes start 4, 6, 2, 4, 1, 3 and look random.
pub fn test_dice(state: State) -> #(Int, State) {
let n = state + 17
let outcome = n * n * n % 1013 % 6 + 1
#(outcome, state + 1)
}Explicit State Handling¶
Here is a Hog simulator in Gleam using explicit state handling, where each function takes the current roll count (the state of the dice) and returns the new one along with its result:
dice.gleamdefines the dice (State,Dice,test_dice) andthree_rolls.hog.gleamimplementsroll_dice,play, and thecatch_upstrategy.
State Passing¶
The state-passing style of functional programming separates state management from program logic:
passing_dice.gleamdefinesStep,run,done, andthen, and reimplementsthree_rollswith them. It imports the dice fromdice.gleam.passing_hog.gleamreimplementsroll_diceandplaywithStep. It imports the strategies fromhog.gleam.
Download the whole project as dice.zip, or view each file below.
To run the main function in a module: gleam run -m <module>, for example
gleam run -m passing_hog.
dice.gleam¶
Source: dice.gleam
//// Pseudo-random dice.
////
//// Instead of having each call to dice() give a different outcome (Python),
//// these dice are functions of the number of times they have been rolled.
//// They return the outcome of the roll along with the new count of rolls.
import gleam/int
import gleam/io
/// The state of the dice: how many rolls so far.
pub type State =
Int
/// Dice take the number of rolls so far and produce an outcome from 1 to 6
/// along with the new number of rolls.
pub type Dice =
fn(State) -> #(Int, State)
/// Six-sided test dice whose outcomes start 4, 6, 2, 4, 1, 3 and look random.
/// Cubing k + 17 modulo 1013 (a prime number) scrambles the outcomes a bit.
pub fn test_dice(state: State) -> #(Int, State) {
let n = state + 17
let outcome = n * n * n % 1013 % 6 + 1
#(outcome, state + 1)
}
/// Example: roll three dice from state and describe the sum, such as
/// "4 + 6 + 2 = 12", along with the state after rolling. The state after each
/// roll is passed to the next roll.
pub fn three_rolls(dice: Dice, state: State) -> #(String, State) {
let #(a, state_a) = dice(state)
let #(b, state_b) = dice(state_a)
let #(c, state_c) = dice(state_b)
let s = int.to_string
let message = s(a) <> " + " <> s(b) <> " + " <> s(c) <> " = " <> s(a + b + c)
#(message, state_c)
}
pub fn main() {
let #(first, state) = three_rolls(test_dice, 0)
io.println(first)
let #(second, _) = three_rolls(test_dice, state)
io.println(second)
}
hog.gleam¶
Source: hog.gleam
//// The Game of Hog, in Gleam.
////
//// Every function that rolls dice takes the current roll count as an argument
//// and returns the new roll count along with its result.
import dice.{type Dice, type State, test_dice}
import gleam/int
import gleam/io
pub const goal = 100
/// Roll num_rolls dice starting from state and return the sum of outcomes,
/// or 1 if a 1 was rolled, along with the state after rolling.
pub fn roll_dice(num_rolls: Int, dice: Dice, state: State) -> #(Int, State) {
roll_and_sum(num_rolls, dice, 0, False, state)
}
/// Roll dice num_rolls more times and add the outcomes to total.
/// The state after each roll is passed to the next roll.
fn roll_and_sum(
num_rolls: Int,
dice: Dice,
total: Int,
rolled_one: Bool,
state: State,
) -> #(Int, State) {
case num_rolls {
0 -> {
let points = case rolled_one {
True -> 1
False -> total
}
#(points, state)
}
_ -> {
let #(outcome, new_state) = dice(state)
roll_and_sum(
num_rolls - 1,
dice,
total + outcome,
rolled_one || outcome == 1,
new_state,
)
}
}
}
/// Play a game starting from state and return the final scores (Player 0,
/// Player 1) along with the state after the game.
pub fn play(
strat0: Strategy,
strat1: Strategy,
score0: Int,
score1: Int,
dice: Dice,
goal: Int,
state: State,
) -> #(#(Int, Int), State) {
case score0 >= goal || score1 >= goal {
True -> #(#(score0, score1), state)
False -> {
let num_rolls = strat0(score0, score1)
let #(points, turn_state) = roll_dice(num_rolls, dice, state)
// Swap scores and strategies for the next turn
let #(scores, final_state) =
play(strat1, strat0, score1, score0 + points, dice, goal, turn_state)
// Unswap the final scores
let #(score1, score0) = scores
#(#(score0, score1), final_state)
}
}
}
/// A strategy takes the player & opponent scores and chooses how many dice to roll.
pub type Strategy =
fn(Int, Int) -> Int
/// A strategy that rolls more dice when the opponent is ahead.
pub fn catch_up(score: Int, opponent_score: Int) -> Int {
case score >= opponent_score {
True -> 4
False -> 5
}
}
pub fn main() {
let #(#(score0, score1), _) =
play(catch_up, catch_up, 0, 0, test_dice, goal, 0)
io.println(int.to_string(score0) <> " to " <> int.to_string(score1))
let winner = case score0 > score1 {
True -> "0"
False -> "1"
}
io.println("Player " <> winner <> " wins!")
}
passing_dice.gleam¶
Source: passing_dice.gleam
//// Dice simulations in state-passing style.
////
//// The dice themselves (State, Dice, and test_dice) come from the dice module.
////
//// The point of Step, run, done, and then is to separate the need to always
//// pass around roll counts from the details of some complex dice simulation.
import dice.{type Dice, type State, test_dice}
import gleam/int
import gleam/io
/// A step in some dice-rolling simulation produces a value and the next state.
/// Each function that constructs part of a simulation returns a Step.
pub type Step(a) =
fn(State) -> #(a, State)
/// Run a step from a starting state and return the value it produces.
pub fn run(step: Step(a), state: State) -> a {
step(state).0
}
/// A step that just produces a value. The state is unchanged.
pub fn done(value: a) -> Step(a) {
fn(state) { #(value, state) }
}
/// A step that runs first, then runs the step that next returns (based on first's result).
/// This can create sequences of steps (using next functions) without mentioning the state.
pub fn then(first: Step(a), next: fn(a) -> Step(b)) -> Step(b) {
fn(state) {
let #(value, state) = first(state)
next(value)(state)
}
}
/// Example: roll three dice and describe the sum, such as "4 + 6 + 2 = 12".
pub fn three_rolls(dice: Dice) -> Step(String) {
then(dice, fn(a) {
then(dice, fn(b) {
then(dice, fn(c) {
let s = int.to_string
done(s(a) <> " + " <> s(b) <> " + " <> s(c) <> " = " <> s(a + b + c))
})
})
})
}
pub fn main() {
io.println(run(three_rolls(test_dice), 0))
io.println(run(three_rolls(test_dice), 3))
}
passing_hog.gleam¶
Source: passing_hog.gleam
//// The Game of Hog in state-passing style.
////
//// Instead of passing the roll count around, build a simulation using
//// Step, run, then, and done.
import dice.{type Dice, test_dice}
import gleam/int
import gleam/io
import hog.{type Strategy, catch_up, goal}
import passing_dice.{type Step, done, run, then}
/// Roll num_rolls dice and return the sum of outcomes, or 1 if a 1 was rolled.
pub fn roll_dice(num_rolls: Int, dice: Dice) -> Step(Int) {
roll_and_sum(num_rolls, dice, 0, False)
}
/// Roll dice num_rolls more times and add the outcomes to total.
fn roll_and_sum(
num_rolls: Int,
dice: Dice,
total: Int,
rolled_one: Bool,
) -> Step(Int) {
case num_rolls {
0 ->
done(case rolled_one {
True -> 1
False -> total
})
_ ->
then(dice, fn(outcome) {
roll_and_sum(
num_rolls - 1,
dice,
total + outcome,
rolled_one || outcome == 1,
)
})
}
}
/// Play a game and produce the final scores: (Player 0, Player 1)
pub fn play(
strat0: Strategy,
strat1: Strategy,
score0: Int,
score1: Int,
dice: Dice,
goal: Int,
) -> Step(#(Int, Int)) {
case score0 >= goal || score1 >= goal {
True -> done(#(score0, score1))
False ->
then(roll_dice(strat0(score0, score1), dice), fn(points) {
// Swap scores and strategies
let rest = play(strat1, strat0, score1, score0 + points, dice, goal)
then(rest, fn(scores) {
// Unswap the final scores
let #(score1, score0) = scores
done(#(score0, score1))
})
})
}
}
pub fn main() {
let #(score0, score1) =
run(play(catch_up, catch_up, 0, 0, test_dice, goal), 0)
io.println(int.to_string(score0) <> " to " <> int.to_string(score1))
let winner = case score0 > score1 {
True -> "0"
False -> "1"
}
io.println("Player " <> winner <> " wins!")
}