Skip to content

Instantly share code, notes, and snippets.

@ababup1192
Last active April 19, 2026 01:09
Show Gist options
  • Select an option

  • Save ababup1192/fa0f6f9dd84c0549ca5a8c4270571cce to your computer and use it in GitHub Desktop.

Select an option

Save ababup1192/fa0f6f9dd84c0549ca5a8c4270571cce to your computer and use it in GitHub Desktop.
Flix Decider pattern
pub type alias Decider[c, e, s] = {
decide = c -> s -> List[e],
evolve = s -> e -> s,
initialState = s,
isTerminal = s -> Bool
}
mod Decider {
pub def fold(decider: Decider[c, e, s], events: List[e]): s =
List.foldLeft((state, event) -> decider#evolve(state, event), decider#initialState, events)
pub def foldFrom(decider: Decider[c, e, s], state: s, events: List[e]): s =
List.foldLeft((st, event) -> decider#evolve(st, event), state, events)
pub def execute(decider: Decider[c, e, s], state: s, command: c): (s, List[e]) =
let events = decider#decide(command, state);
let newState = foldFrom(decider, state, events);
(newState, events)
pub def executeAll(decider: Decider[c, e, s], state: s, commands: List[c]): (s, List[e]) =
List.foldLeft((acc, cmd) -> {
let (currentState, accEvents) = acc;
let (newState, newEvents) = execute(decider, currentState, cmd);
(newState, List.append(accEvents, newEvents))
}, (state, Nil), commands)
pub def executeAllFromInitial(decider: Decider[c, e, s], commands: List[c]): (s, List[e]) =
executeAll(decider, decider#initialState, commands)
}
// ============================================================================
// 本の貸出ドメイン
// ============================================================================
pub enum BookCommand with Eq, ToString {
case Register // 本を登録する
case CheckOut(String) // 本を貸し出す(borrowerId)
case Return // 本を返却する
}
pub enum BookEvent with Eq, ToString {
case Registered // 書籍登録イベント
case CheckedOut(String) // 書籍貸出イベント(borrowerId)
case Returned // 書籍返却イベント
}
pub enum BookState with Eq, ToString {
case UnregisteredBook // 未登録本
case AvailableBook // 貸出可能本
case CheckedOutBook(String) // 貸出中(borrowerId)
}
mod BookLending {
use BookCommand.{Register, CheckOut, Return}
use BookEvent.{Registered, CheckedOut, Returned}
use BookState.{UnregisteredBook, AvailableBook, CheckedOutBook}
pub def decide(command: BookCommand, state: BookState): List[BookEvent] =
match (command, state) {
case (Register, UnregisteredBook) =>
Registered :: Nil
case (CheckOut(borrowerId), AvailableBook) =>
CheckedOut(borrowerId) :: Nil
case (Return, CheckedOutBook(_)) =>
Returned :: Nil
case _ =>
Nil // 無効な操作
}
pub def evolve(state: BookState, event: BookEvent): BookState =
match (state, event) {
case (UnregisteredBook, Registered) =>
AvailableBook
case (AvailableBook, CheckedOut(borrowerId)) =>
CheckedOutBook(borrowerId)
case (CheckedOutBook(_), Returned) =>
AvailableBook
case _ =>
state // 無効なイベントは状態を変更しない
}
pub def initialState(): BookState = UnregisteredBook
pub def isTerminal(_state: BookState): Bool = false
pub def decider(): Decider[BookCommand, BookEvent, BookState] = {
decide = decide,
evolve = evolve,
initialState = initialState(),
isTerminal = isTerminal
}
pub def getBorrower(state: BookState): Option[String] =
match state {
case CheckedOutBook(borrowerId) => Some(borrowerId)
case _ => None
}
pub def isAvailable(state: BookState): Bool =
match state {
case AvailableBook => true
case _ => false
}
}
// ============================================================================
// イベントストア(インメモリ)
// ============================================================================
mod EventStore {
pub def load(store: Map[String, List[e]], streamId: String): List[e] =
Map.getWithDefault(streamId, Nil, store)
pub def append(store: Map[String, List[e]], streamId: String, events: List[e]): Map[String, List[e]] =
let existing = load(store, streamId);
let updated = List.append(existing, events);
Map.insert(streamId, updated, store)
}
// ============================================================================
// ファイルベースイベントストア(DB代わり)
// ============================================================================
mod FileStore {
import java.io.{File, FileWriter, BufferedWriter, FileReader, BufferedReader}
pub def loadEvents(path: String): List[BookEvent] \ IO =
let file = new File(path);
if (file.exists()) {
let reader = new BufferedReader(new FileReader(file));
let lines = readAllLines(reader, Nil);
reader.close();
List.filterMap(line -> parseLine(line), lines)
} else {
Nil
}
def readAllLines(reader: BufferedReader, acc: List[String]): List[String] \ IO =
let line = reader.readLine();
if (Object.isNull(line)) {
List.reverse(acc)
} else {
readAllLines(reader, String.trim(line) :: acc)
}
pub def appendEvents(path: String, events: List[BookEvent]): Unit \ IO =
let file = new File(path);
let parent = file.getParentFile();
if (not Object.isNull(parent) and not parent.exists()) {
discard parent.mkdirs()
} else {
()
};
let writer = new BufferedWriter(new FileWriter(file, true));
List.forEach(event -> {
writer.write(eventToString(event));
writer.newLine()
}, events);
writer.close()
pub def eventToString(event: BookEvent): String =
match event {
case BookEvent.Registered => "Registered"
case BookEvent.CheckedOut(borrowerId) => "CheckedOut(${borrowerId})"
case BookEvent.Returned => "Returned"
}
def parseLine(line: String): Option[BookEvent] =
if (String.isEmpty(line)) {
None
} else if (line == "Registered") {
Some(BookEvent.Registered)
} else if (line == "Returned") {
Some(BookEvent.Returned)
} else {
parseCheckedOut(line)
}
def parseCheckedOut(line: String): Option[BookEvent] =
let prefix = "CheckedOut(";
let prefixLen = String.length(prefix);
if (String.indexOf({substr = prefix}, line) == Some(0)) {
let len = String.length(line);
if (len > prefixLen + 1) {
let borrowerId = String.sliceRight({start = prefixLen}, String.sliceLeft({end = len - 1}, line));
Some(BookEvent.CheckedOut(borrowerId))
} else {
None
}
} else {
None
}
}
eff BookEventStoreEff {
def loadBookEvents(streamId: String): List[BookEvent]
def appendBookEvents(streamId: String, events: List[BookEvent]): Unit
}
eff BookDecisionEff {
def onBookDecide(streamId: String, command: BookCommand, events: List[BookEvent]): Unit
def onBookReject(streamId: String, command: BookCommand, reason: String): Unit
}
mod EffectfulRunner {
pub def execute(
decider: Decider[BookCommand, BookEvent, BookState],
streamId: String,
command: BookCommand
): List[BookEvent] \ BookEventStoreEff + BookDecisionEff =
let events = BookEventStoreEff.loadBookEvents(streamId);
let currentState = Decider.fold(decider, events);
let newEvents = decider#decide(command, currentState);
match newEvents {
case Nil =>
BookDecisionEff.onBookReject(streamId, command, "No events generated");
Nil
case _ =>
BookEventStoreEff.appendBookEvents(streamId, newEvents);
BookDecisionEff.onBookDecide(streamId, command, newEvents);
newEvents
}
pub def executeAll(
decider: Decider[BookCommand, BookEvent, BookState],
streamId: String,
commands: List[BookCommand]
): List[BookEvent] \ BookEventStoreEff + BookDecisionEff =
List.flatMap(cmd -> execute(decider, streamId, cmd), commands)
pub def getState(
decider: Decider[BookCommand, BookEvent, BookState],
streamId: String
): BookState \ BookEventStoreEff =
let events = BookEventStoreEff.loadBookEvents(streamId);
Decider.fold(decider, events)
}
// ============================================================================
// メイン関数
// ============================================================================
def main(): Unit \ IO =
println("=== Functional Event Sourcing - Book Lending System ===");
println("");
let decider = BookLending.decider();
// === デモ1: InMemory ストア ===
println("--- Demo 1: InMemory Store ---");
region rc {
let store = Ref.fresh(rc, Map.empty());
run {
discard EffectfulRunner.execute(decider, "book-001", BookCommand.Register);
discard EffectfulRunner.execute(decider, "book-001", BookCommand.CheckOut("alice"));
discard EffectfulRunner.execute(decider, "book-001", BookCommand.Return);
discard EffectfulRunner.execute(decider, "book-001", BookCommand.CheckOut("bob"));
discard EffectfulRunner.execute(decider, "book-002", BookCommand.CheckOut("charlie"));
let book1State = EffectfulRunner.getState(decider, "book-001");
let book2State = EffectfulRunner.getState(decider, "book-002");
println("book-001 state: ${book1State}");
println("book-002 state: ${book2State}");
match BookLending.getBorrower(book1State) {
case Some(borrower) => println("Currently borrowed by: ${borrower}")
case None => println("Not currently borrowed")
}
}
// [INFO ] [DECIDE] book-001: Register -> Registered :: Nil
// [INFO ] [DECIDE] book-001: CheckOut(alice) -> CheckedOut(alice) :: Nil
// [INFO ] [DECIDE] book-001: Return -> Returned :: Nil
// [INFO ] [DECIDE] book-001: CheckOut(bob) -> CheckedOut(bob) :: Nil
// [WARN ] [REJECT] book-002: CheckOut(charlie) - No events generated
// book-001 state: CheckedOutBook(bob)
// book-002 state: UnregisteredBook
// Currently borrowed by: bob
with handler BookEventStoreEff {
def loadBookEvents(streamId, k) = k(EventStore.load(Ref.get(store), streamId))
def appendBookEvents(streamId, events, k) = {
Ref.put(EventStore.append(Ref.get(store), streamId, events), store);
k()
}
}
with handler BookDecisionEff {
def onBookDecide(streamId, command, events, k) = {
Logger.info("[DECIDE] ${streamId}: ${command} -> ${events}");
k()
}
def onBookReject(streamId, command, reason, k) = {
Logger.warn("[REJECT] ${streamId}: ${command} - ${reason}");
k()
}
}
with Logger.runWithIO
};
println("");
// === デモ2: File ストア ===
println("--- Demo 2: File Store ---");
let baseDir = "events";
run {
discard EffectfulRunner.execute(decider, "book-101", BookCommand.Register);
discard EffectfulRunner.execute(decider, "book-101", BookCommand.CheckOut("dave"));
let bookState = EffectfulRunner.getState(decider, "book-101");
println("book-101 state after checkout: ${bookState}")
}
// [INFO ] [DECIDE] book-101: Register -> Registered :: Nil
// [INFO ] [DECIDE] book-101: CheckOut(dave) -> CheckedOut(dave) :: Nil
// book-101 state after checkout: CheckedOutBook(dave)
with handler BookEventStoreEff {
def loadBookEvents(streamId, k) = k(FileStore.loadEvents("${baseDir}/${streamId}.txt"))
def appendBookEvents(streamId, events, k) = {
FileStore.appendEvents("${baseDir}/${streamId}.txt", events);
k()
}
}
with handler BookDecisionEff {
def onBookDecide(streamId, command, events, k) = {
Logger.info("[DECIDE] ${streamId}: ${command} -> ${events}");
k()
}
def onBookReject(streamId, command, reason, k) = {
Logger.warn("[REJECT] ${streamId}: ${command} - ${reason}");
k()
}
}
with Logger.runWithIO;
println("");
// ファイルから復元(再起動シミュレーション)
println("--- Reload from File (Simulating Restart) ---");
run {
discard EffectfulRunner.execute(decider, "book-101", BookCommand.Return);
let bookState = EffectfulRunner.getState(decider, "book-101");
println("book-101 state after restart + return: ${bookState}")
}
// [INFO ] [DECIDE] book-101: Return -> Returned :: Nil
// book-101 state after restart + return: AvailableBook
with handler BookEventStoreEff {
def loadBookEvents(streamId, k) = k(FileStore.loadEvents("${baseDir}/${streamId}.txt"))
def appendBookEvents(streamId, events, k) = {
FileStore.appendEvents("${baseDir}/${streamId}.txt", events);
k()
}
}
with handler BookDecisionEff {
def onBookDecide(streamId, command, events, k) = {
Logger.info("[DECIDE] ${streamId}: ${command} -> ${events}");
k()
}
def onBookReject(streamId, command, reason, k) = {
Logger.warn("[REJECT] ${streamId}: ${command} - ${reason}");
k()
}
}
with Logger.runWithIO
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment