State Management #

Ref, SynchronizedRef, SubscriptionRef, and TxRef: shared state with atomic updates instead of mutable variables.

The problem. A shared variable and concurrency do not work together. The error hides behind any await:

let count = 0
await Promise.all(items.map(async () => {
  const current = count
  await saveToDb()       // any pause here...
  count = current + 1    // ...and every task writes the same stale value
}))
console.log(count)       // 1, not 100

Each task read 0, paused, and wrote 1. The program lost 99 updates. It threw no error, and the code looks correct. Later you want to react when the count changes. You add an EventEmitter. Now you have a second system, with listeners that can leak and with its own order problems.

The shift

Today you think of state as a variable that you change. You think of change notification as an emitter that you add next to the variable. In Effect, state is a value inside a container. Each operation on the container is an effect. You do not write count = count + 1. You describe an update, Ref.update(count, (n) => n + 1), and Effect applies that update as 1 atomic step. Atomic means that no other fiber can act between the read and the write. A pure function has no pause, so nothing can separate the read from the write.

Reads and updates are effects, so they combine with all the tools from the earlier sections. You can run them concurrently, retry them, interrupt them, and give them the lifetime of a service. Some cases need more: the update is an effect, something must react to each change, or several values must change together. Each case has its own container.

Container The update is... Use it when
let Read, pause, write Never for state that fibers share
Ref A pure function, applied as 1 atomic step A counter, a cache, a list of recorded calls
SynchronizedRef An effect, run under a lock The new value comes from a fetch or from a computation that can fail
SubscriptionRef The same as Ref, plus a stream of changes Something must react to each change: a status, a progress value, a config
TxRef + Effect.tx Several refs change together, or not at all Money, inventory, any 2 values that must stay consistent

This section starts with the lost update error. It corrects the error with Ref. Then it goes through the table, 1 row at a time.

Learn #

Lesson 1. Lost updates, then Ref #

The program below first runs the error from the intro inside Effect. Fibers have the same problem as Promises. 100 fibers each read a plain let, pause for 1 millisecond, and write the value back. All of them read 0, so the final value is 1.

Then the program does the same work with a Ref. Ref.make(0) makes the container. It is an effect, so you yield* it. Ref.update(ref, f) applies f to the current value and stores the result as 1 atomic step. No other fiber can act between the read and the write, because from the outside there is only 1 operation.

Ref.get reads the current value. Like each operation here, it is an effect until you run it.

import { Effect, Ref } from "effect"

const ids = Array.from({ length: 100 }, (_, i) => i)

const program = Effect.gen(function* () {
  // A plain variable: read, pause, write. Every fiber reads 0 before any of them writes.
  let plain = 0
  yield* Effect.forEach(ids, () =>
    Effect.gen(function* () {
      const current = plain
      yield* Effect.sleep("1 millis")
      plain = current + 1
    }), { concurrency: "unbounded", discard: true })
  console.log("let:", plain)

  // A Ref: update is one atomic step. Nothing can slip in between read and write.
  const ref = yield* Ref.make(0)
  yield* Effect.forEach(ids, () => Ref.update(ref, (n) => n + 1), { concurrency: "unbounded", discard: true })
  console.log("Ref:", yield* Ref.get(ref))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

Note: the Ref version has no sleep. That is intentional. update accepts a plain function, so there is no place for a pause between the read and the write. That limit is the guarantee. Lesson 4 shows what to do when the update itself must wait.

Lesson 2. The Ref operations #

Ref has a small set of operations. They differ only in the value that they return.

Operation Does Returns
Ref.get(ref) Reads The current value
Ref.set(ref, a) Replaces void
Ref.update(ref, f) Applies f void
Ref.updateAndGet(ref, f) Applies f The new value
Ref.getAndUpdate(ref, f) Applies f The old value
Ref.modify(ref, f) f returns [result, newValue] The result

modify is the general operation. In 1 atomic step, you compute a value to return and a value to store. "Take 1 item from the stock and tell me if it worked" is a modify. If you write it as get, a check, and set, 2 fibers can both see 1 item and both take it.

import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const stock = yield* Ref.make(3)

  // modify: decide AND update in one atomic step. Returns the decision.
  const take = Ref.modify(stock, (n) => (n > 0 ? [true, n - 1] : [false, n]))

  console.log("take:", yield* take)
  console.log("take:", yield* take)
  console.log("getAndUpdate old:", yield* Ref.getAndUpdate(stock, (n) => n + 5))
  console.log("updateAndGet new:", yield* Ref.updateAndGet(stock, (n) => n - 1))

  yield* Ref.set(stock, 0)
  console.log("take:", yield* take)
  console.log("left:", yield* Ref.get(stock))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

The program defines take 1 time and runs it 3 times. Each run reads the current stock again. Change the pair in modify to [n - 1, true]: the compiler rejects it. The second element must have the stored type, number.

Lesson 3. State that belongs to a service #

In plain TypeScript, private state lives in a class:

class IdGenerator {
  private counter = 0
  next() { return "order-" + ++this.counter }
}

The Effect form is a factory. A factory is an effect that makes a Ref and returns functions that use it. Each run of the factory gives a new instance. If you run it 2 times, you get 2 independent counters. Each stateful service in the later sections uses this pattern.

The same pattern makes tests exact. A fake mailer records each call in a Ref<Array<string>>. A test can then check "exactly 2 emails, to these addresses" without a mock library. The record is a Ref, so it is safe when the code under test sends emails concurrently.

import { Effect, Ref } from "effect"

// A factory: the Ref is created once, the returned Effect closes over it
const makeIdGenerator = (prefix: string) =>
  Effect.gen(function* () {
    const counter = yield* Ref.make(0)
    return {
      next: Ref.updateAndGet(counter, (n) => n + 1).pipe(Effect.map((n) => prefix + "-" + n))
    }
  })

// A fake for tests: records calls instead of sending anything
const makeFakeMailer = Effect.gen(function* () {
  const sent = yield* Ref.make<Array<string>>([])
  return {
    send: (to: string) => Ref.update(sent, (list) => [...list, to]),
    sentTo: Ref.get(sent)
  }
})

const program = Effect.gen(function* () {
  const orders = yield* makeIdGenerator("order")
  const users = yield* makeIdGenerator("user")   // a second, independent counter
  console.log(yield* orders.next, yield* orders.next, yield* users.next)

  const mailer = yield* makeFakeMailer
  yield* mailer.send("ada@example.com")
  yield* mailer.send("lin@example.com")
  const sent = yield* mailer.sentTo
  console.log("sent", sent.length, "emails:", sent.join(", "))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

orders.next is 1 effect value. The program runs it 2 times and gets 2 different ids, because the Ref changed between the runs. Move Ref.make(0) inside next: each call then starts from a new zero. The place where you make the Ref decides how long the state lives.

Lesson 4. SynchronizedRef: when the update is an effect #

Ref.update accepts a pure function. What if the new value comes from an effect, for example a price lookup or a call that can fail? You cannot yield* inside update. A common workaround is get, then run the effect, then set. That is 3 steps again. The lost update error returns as soon as the middle step pauses.

SynchronizedRef exists for this case. SynchronizedRef.updateEffect(ref, f) accepts a function that returns an effect. It runs the full sequence, read, effect, write, under a lock. A lock permits 1 fiber at a time; the other fibers wait. modifyEffect is the modify version. The pure operations get, set, update, and modify also exist.

Note: the updates run 1 at a time. That is the intent. An effectful update that can interleave is an update that can be lost.

import { Effect, Ref, SynchronizedRef } from "effect"

const ids = Array.from({ length: 100 }, (_, i) => i)

// A lookup that takes time, like fetching the next value from a service
const lookupNext = (n: number) => Effect.sleep("1 millis").pipe(Effect.as(n + 1))

const program = Effect.gen(function* () {
  // Ref: get, then an Effect, then set. Three steps, so updates are lost again.
  const ref = yield* Ref.make(0)
  yield* Effect.forEach(ids, () =>
    Effect.gen(function* () {
      const current = yield* Ref.get(ref)
      const next = yield* lookupNext(current)
      yield* Ref.set(ref, next)
    }), { concurrency: "unbounded", discard: true })
  console.log("Ref get/set:", yield* Ref.get(ref))

  // SynchronizedRef.updateEffect: the whole read-effect-write runs under a lock
  const sref = yield* SynchronizedRef.make(0)
  yield* Effect.forEach(ids, () => SynchronizedRef.updateEffect(sref, lookupNext), {
    concurrency: "unbounded",
    discard: true
  })
  console.log("SynchronizedRef:", yield* SynchronizedRef.get(sref))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

The second half takes more time than the first half. 100 lookups of 1 ms now run 1 after the other, not all at the same time. That is the cost of a correct result. If the lookup fails, updateEffect fails with that error, and the stored value does not change.

Lesson 5. SubscriptionRef: state that you can watch #

Some state is not only read; something must watch it. Examples: a job status, an upload progress value, a config value that reloads. SubscriptionRef is a Ref with 1 more operation, SubscriptionRef.changes(ref). This returns a Stream. The stream first emits the current value, and then each new value when it is set.

An observer is a stream pipeline:

  • Stream.tap reacts to each value.
  • Stream.takeUntil decides when to stop.
  • Stream.runDrain runs the stream to the end.

Fork the observer with Effect.forkChild, so that it runs next to the writer. Join it with Fiber.join at the end.

Note: a stream subscribes when it starts to run, and a forked fiber does not start before the parent continues. The stream does not replay writes that happened before the subscription. The Deferred in the program is a signal that completes 1 time. The observer completes it on its first value. The writer waits for it before the first write. Without this signal, the observer sees only the last value.

import { Deferred, Effect, Fiber, Stream, SubscriptionRef } from "effect"

const program = Effect.gen(function* () {
  const status = yield* SubscriptionRef.make("idle")
  const subscribed = yield* Deferred.make<void>()

  // The observer: every value, starting with the current one, until "done"
  const observer = yield* SubscriptionRef.changes(status).pipe(
    Stream.tap((s) => Effect.sync(() => console.log("status:", s))),
    Stream.tap(() => Deferred.succeed(subscribed, void 0)),   // signal: I am listening
    Stream.takeUntil((s) => s === "done"),
    Stream.runDrain,
    Effect.forkChild
  )

  yield* Deferred.await(subscribed)   // do not write before the observer is attached
  yield* SubscriptionRef.set(status, "loading")
  yield* SubscriptionRef.update(status, (s) => (s === "loading" ? "saving" : s))
  yield* SubscriptionRef.set(status, "done")

  yield* Fiber.join(observer)
  console.log("observer finished, final:", yield* SubscriptionRef.get(status))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

The program printed "idle", but no write set that value. changes starts with the current value, so a late observer still gets the present state. Remove the Deferred.await line and run the program again: the observer subscribes after the 3 writes and prints only "done".

Lesson 6. TxRef and Effect.tx: several values, 1 change #

A bank transfer changes 2 balances. If the debit is stored and the credit is not, the money is lost. Ref cannot prevent this. Each Ref is atomic by itself, but 2 Refs together are not.

TxRef is a transactional ref. Its operations look like the Ref operations, with 1 difference. Inside Effect.tx(...), Effect records each read and write in a journal. It stores the writes only when the body completes. If the body fails, Effect stores nothing, not even the writes that happened earlier in the body. If another transaction changes one of the same values first, this transaction starts again with new reads. For this reason, the program can run 70 transfers concurrently, each with a pause inside, and lose nothing.

A single TxRef.get or TxRef.set outside Effect.tx is a small transaction by itself. Effect.tx puts several operations into 1 transaction.

import { Effect, TxRef } from "effect"

const program = Effect.gen(function* () {
  const alice = yield* TxRef.make(100)
  const bob = yield* TxRef.make(0)
  const transfers = yield* TxRef.make(0)

  const transfer = (from: TxRef.TxRef<number>, to: TxRef.TxRef<number>, amount: number) =>
    Effect.tx(Effect.gen(function* () {
      yield* TxRef.update(transfers, (n) => n + 1)     // written first...
      const balance = yield* TxRef.get(from)
      if (balance < amount) return yield* Effect.fail("insufficient funds")   // ...discarded on failure
      yield* Effect.sleep("1 millis")                   // a pause, so concurrent transfers overlap
      yield* TxRef.set(from, balance - amount)
      yield* TxRef.update(to, (n) => n + amount)
    }))

  const report = (label: string) =>
    Effect.gen(function* () {
      const [a, b, t] = yield* Effect.all([TxRef.get(alice), TxRef.get(bob), TxRef.get(transfers)])
      console.log(label + ": alice " + a + ", bob " + b + ", transfers " + t)
    })

  yield* transfer(alice, bob, 30)
  yield* report("after 30")

  const outcome = yield* transfer(alice, bob, 500).pipe(Effect.result)
  yield* report("after 500 (" + outcome._tag + ")")

  // 70 concurrent transfers of 1. Conflicts retry; money is neither created nor lost.
  yield* Effect.forEach(Array.from({ length: 70 }), () => transfer(alice, bob, 1), {
    concurrency: "unbounded",
    discard: true
  })
  yield* report("after 70 x 1")
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

The failed transfer increased transfers and then failed. The count stayed at 1: Effect discarded that write together with the rest of the transaction. Remove the Effect.tx call and run the program again. With the pause inside, concurrent transfers read old balances, and the sum is no longer 100.

Do and don't #

DoDon'tWhy
Keep state that fibers share in a Ref.Do not share a let variable between fibers.A pause between the read and the write lets other fibers write old values, and updates are lost.
Use Ref.update or Ref.modify for a change that depends on the current value.Do not call Ref.get, compute a new value, and then call Ref.set.The read and the write are 2 steps, so 2 fibers can read the same value and 1 update is lost.
Return [result, newValue] from Ref.modify, in that order.Do not return [newValue, result].The first element goes to the caller and the second element is stored, so a swap stores the wrong value or does not compile.
Call Ref.make 1 time, in the factory effect that builds the service.Do not call Ref.make inside the function that uses the ref.Each call then makes a new ref with the initial value, and the state resets on each call.
Use SynchronizedRef.updateEffect when the new value comes from an effect.Do not use Ref.get, then an effect, then Ref.set for an effectful update.The effect between the read and the write pauses, and concurrent updates are lost.
Wait for a Deferred signal from the observer before you write to a SubscriptionRef.Do not write directly after Effect.forkChild of the observer.The forked fiber has not subscribed yet, and changes does not replay earlier values, so the observer misses them.
Wrap several TxRef operations in 1 Effect.tx call.Do not run TxRef.get and TxRef.set on 2 refs as separate calls.Each call is its own transaction, so a pause between them lets another transfer read old balances, and the totals become wrong.

Fix it #

Each program below is broken or incomplete. Make it print the expected output with zero type errors. Use hints before the solution.

1. Read, pause, write #

50 fibers each do some work and then add 1 to a counter. The sleep represents the work and must stay. The counter ends at 1. Change the counter update so that the program prints counted: 50.

expected output: counted: 50
import { Effect, Ref } from "effect"

const ids = Array.from({ length: 50 }, (_, i) => i)

const program = Effect.gen(function* () {
  const counter = yield* Ref.make(0)

  yield* Effect.forEach(ids, () =>
    Effect.gen(function* () {
      const current = yield* Ref.get(counter)
      yield* Effect.sleep("1 millis")          // the work
      yield* Ref.set(counter, current + 1)
    }), { concurrency: "unbounded", discard: true })

  console.log("counted:", yield* Ref.get(counter))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

2. An update that waits #

The program must add the price of each item to the total. The price comes from a lookup that takes time. The file does not compile: the update function returns an effect, but the operation expects a plain number. Use the operation that is made for effectful updates. The program must print total: 6.

expected output: total: 6
import { Effect, SynchronizedRef } from "effect"

const lookupPrice = (item: string) =>
  Effect.sleep("1 millis").pipe(Effect.as(item.length))

const program = Effect.gen(function* () {
  const total = yield* SynchronizedRef.make(0)

  yield* Effect.forEach(["ab", "cd", "ef"], (item) =>
    SynchronizedRef.update(total, (sum) => lookupPrice(item).pipe(Effect.map((price) => sum + price))),
    { concurrency: "unbounded", discard: true })

  console.log("total:", yield* SynchronizedRef.get(total))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

3. Off by 1 id #

nextId must give 1, 2, 3. It gives 0, 1, 2. The stored counter is correct; the returned value is not. Change the modify call so that the program prints ids: 1 2 3.

expected output: ids: 1 2 3
import { Effect, Ref } from "effect"

const program = Effect.gen(function* () {
  const counter = yield* Ref.make(0)
  const nextId = Ref.modify(counter, (n) => [n, n + 1])

  const a = yield* nextId
  const b = yield* nextId
  const c = yield* nextId
  console.log("ids:", a, b, c)
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

4. A new Ref on each call #

The generator must give ids that increase, but each call returns 1. The program makes the Ref in the wrong place. Change the structure of makeIdGenerator so that the program prints 1 2 3.

expected output: 1 2 3
import { Effect, Ref } from "effect"

const makeIdGenerator = Effect.succeed({
  next: Effect.gen(function* () {
    const counter = yield* Ref.make(0)
    return yield* Ref.updateAndGet(counter, (n) => n + 1)
  })
})

const program = Effect.gen(function* () {
  const gen = yield* makeIdGenerator
  console.log(yield* gen.next, yield* gen.next, yield* gen.next)
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

5. The observer that starts too late #

The observer must print each progress value from 0 to 3. It prints only the last value, because it subscribes after the writes. Add the signal from lesson 5 so that the program prints all 4 lines and then finished.

expected output: progress: 0 progress: 1 progress: 2 progress: 3 finished
import { Effect, Fiber, Stream, SubscriptionRef } from "effect"

const program = Effect.gen(function* () {
  const progress = yield* SubscriptionRef.make(0)

  const observer = yield* SubscriptionRef.changes(progress).pipe(
    Stream.tap((n) => Effect.sync(() => console.log("progress:", n))),
    Stream.takeUntil((n) => n === 3),
    Stream.runDrain,
    Effect.forkChild
  )

  yield* SubscriptionRef.set(progress, 1)
  yield* SubscriptionRef.set(progress, 2)
  yield* SubscriptionRef.set(progress, 3)

  yield* Fiber.join(observer)
  console.log("finished")
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

6. Money that appears from nothing #

50 concurrent transfers of 1 must move 50 from alice to bob. The total must stay at 100. Instead, the total increases. The transfer reads and writes the refs as separate steps. Make the transfer 1 atomic change so that the program prints alice 50, bob 50, total 100.

expected output: alice 50, bob 50, total 100
import { Effect, TxRef } from "effect"

const program = Effect.gen(function* () {
  const alice = yield* TxRef.make(100)
  const bob = yield* TxRef.make(0)

  const transfer = Effect.gen(function* () {
    const balance = yield* TxRef.get(alice)
    yield* Effect.sleep("1 millis")
    yield* TxRef.set(alice, balance - 1)
    yield* TxRef.update(bob, (n) => n + 1)
  })

  yield* Effect.forEach(Array.from({ length: 50 }), () => transfer, { concurrency: "unbounded", discard: true })

  const [a, b] = yield* Effect.all([TxRef.get(alice), TxRef.get(bob)])
  console.log("alice " + a + ", bob " + b + ", total " + (a + b))
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

Build it #

Write the program from the spec. The output must match exactly.

1. Inventory reservations #

Build a small inventory on top of a Ref<Record<string, number>> that starts at { apple: 3, pear: 2 }.

  • reserve(item, qty) uses Ref.modify to check and update in 1 atomic step. If the stock is sufficient, subtract qty and return "ok". If not, keep the stock unchanged and return "out of stock".
  • Process the requests [["apple", 2], ["pear", 1], ["apple", 2], ["apple", 1]] in order. Print 1 line per request, then the final stock.

Exact output:

reserve apple x2: ok
reserve pear x1: ok
reserve apple x2: out of stock
reserve apple x1: ok
stock: apple=0 pear=1
expected output: reserve apple x2: ok reserve pear x1: ok reserve apple x2: out of stock reserve apple x1: ok stock: apple=0 pear=1
import { Effect, Ref } from "effect"

const requests: Array<[string, number]> = [["apple", 2], ["pear", 1], ["apple", 2], ["apple", 1]]

const program = Effect.gen(function* () {
  const stock = yield* Ref.make<Record<string, number>>({ apple: 3, pear: 2 })

  // TODO: reserve(item, qty) with Ref.modify: returns "ok" or "out of stock"

  // TODO: loop over requests, print "reserve <item> x<qty>: <result>"

  // TODO: print "stock: apple=<n> pear=<n>"
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

2. Progress reporter #

Process 3 files and report the progress through a SubscriptionRef<number>.

  • processFile(name) is given. It sleeps for a short time and returns.
  • Start an observer with SubscriptionRef.changes. It prints progress: <n>/3 for each value, with the initial 0 included, and stops after 3. Fork it. Use a Deferred signal so that the observer misses no update.
  • Process the files in order with Effect.forEach. Add 1 to the ref after each file.
  • Join the observer, then print all files processed.

Exact output:

progress: 0/3
progress: 1/3
progress: 2/3
progress: 3/3
all files processed
expected output: progress: 0/3 progress: 1/3 progress: 2/3 progress: 3/3 all files processed
import { Deferred, Effect, Fiber, Stream, SubscriptionRef } from "effect"

const files = ["a.txt", "b.txt", "c.txt"]
const processFile = (name: string) => Effect.sleep("1 millis")

const program = Effect.gen(function* () {
  const progress = yield* SubscriptionRef.make(0)

  // TODO: Deferred handshake + forked observer printing "progress: <n>/3", stopping at 3

  // TODO: await the handshake, then process the files, incrementing progress after each

  // TODO: join the observer and print "all files processed"
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

3. Bank ledger #

Model 3 accounts as TxRef<number> values: Ada 100, Lin 50, Sam 0.

  • transfer(from, to, amount) is 1 Effect.tx transaction. It fails with the string "insufficient funds" if from holds less than amount. If not, it debits from and credits to. Put an Effect.sleep("1 millis") between the read and the writes, so that concurrent transfers overlap.
  • Run Ada -> Lin 30 and then Lin -> Sam 100. Print ok or the error message for each one. Use Effect.result.
  • Then run 20 concurrent Ada -> Sam 1 transfers with Effect.forEach and concurrency: "unbounded". Print done.
  • At the end, print the balances and their total.

Exact output:

Ada -> Lin 30: ok
Lin -> Sam 100: insufficient funds
Ada -> Sam 1 x 20: done
Ada=50 Lin=80 Sam=20 total=150
expected output: Ada -> Lin 30: ok Lin -> Sam 100: insufficient funds Ada -> Sam 1 x 20: done Ada=50 Lin=80 Sam=20 total=150
import { Effect, TxRef } from "effect"

const program = Effect.gen(function* () {
  const ada = yield* TxRef.make(100)
  const lin = yield* TxRef.make(50)
  const sam = yield* TxRef.make(0)

  // TODO: transfer(from, to, amount): Effect.tx, fail with "insufficient funds", sleep 1 ms between read and writes

  // TODO: Ada -> Lin 30, Lin -> Sam 100, printing "ok" or the error

  // TODO: 20 concurrent Ada -> Sam 1, then print "Ada -> Sam 1 x 20: done"

  // TODO: print "Ada=<n> Lin=<n> Sam=<n> total=<n>"
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

Recall #

Answer in your head first, then reveal. Come back to these tomorrow.

Why does a `let` that fibers share lose updates, and why does `Ref.update` not? #

With let, the read and the write are separate steps. A pause between them lets other fibers read the same old value. Ref.update(ref, f) applies f and stores the result as 1 atomic operation. f is a pure function, so there is no pause where another fiber can act.

What is the type of `Ref.modify(ref, (n) => [n > 0, n - 1])`, for `ref: Ref<number>`? #

Effect<boolean>. modify accepts (A) => [B, A]. It returns the B (here the boolean) and stores the A (here n - 1).

Which container do you use when the new value comes from an HTTP call? #

SynchronizedRef with updateEffect or modifyEffect. The update function returns an effect. The read, the effect, and the write run under a lock, so concurrent updates run 1 at a time and are not lost. A plain Ref cannot run an effect inside update.

What does `SubscriptionRef.changes(ref)` emit first, and what does it never emit? #

It emits the current value first, then each new value when it is set. It never replays values that were set before the stream subscribed. That is why a forked observer needs a signal (a Deferred) before the writer starts.

What does `Effect.tx` add on top of the single `TxRef` operations? #

It puts them into 1 transaction. Each TxRef.get, set, or update by itself is a transaction with 1 step. Effect.tx(body) records each read and write in the body in a journal and stores them together. A failure discards all of them. If another transaction changes one of the same values first, this transaction starts again with new reads.

Where must you call `Ref.make` so that the state of a service lives as long as the service? #

1 time, in the factory effect that builds the service, before it returns the functions that use the Ref. If Ref.make is inside one of those functions, each call makes a new Ref, and the state resets on each call.