Calculator: In-Memory FSM
Learn how to define and use an in-memory finite state machine through a calculator whose current state selects its behavior.
Examplefsmfinite-state-machinein-memorystatetransitioncontextcalculatorexpressionsapirestui
🌅 Horizon
Calculator FSM at a Glance
Learning Scenario
Imagine a calculator where you choose the operation once. It remembers whether it is adding, subtracting, multiplying, or dividing, and every later calculation follows that active mode until you change it.
This small example makes state visible and practical. The finite state machine owns the current mode, a context stores its description, and each state selects the matching calculation behavior.
Architecture
What It Demonstrates
- An in-memory FSM with a default state.
- Events, transitions, and state lifecycle actions.
- A shared event handler available from every state.
- Context updates and state-specific expression routing.
- Plain and validated calculation results.
Expected Result
You will be able to select a mode, inspect it, and calculate through the behavior of the active state. Before a mode is selected, the calculator safely returns an invalid or zero result.
🧭 Voyage
1. Model a Validated Result
Most calculations can return a number directly. Some failures, such as division by zero, also need to tell the caller whether that number is meaningful. We will model both pieces together.
@datatype
CalcResult
result: Float
isValid: Boolean
result carries the calculated value, while
isValid makes success or failure explicit. The FSM will use
this datatype for its validated calculation event.
2. Define the Calculator API
The API exposes the four interactions needed to explore the state machine: inspect the current mode, change it, calculate a plain value, or calculate a validated result.
@api
CalcApi style: rest
engine = gin
configType = ApiConfig
basePath = /
generateSwagger = true
get /mode getMode(_) : String
post /set-op/{op} setOperation(op:String) : String
post /calc/{a}/{b} calculate(a:Float, b:Float) : Float
post /calc-valid/{a}/{b} calculateWithValidation(a:Float, b:Float) : CalcResult
Notice that calculate does not receive an operation. That is
intentional: the active FSM state decides which operation runs.
setOperation changes that state beforehand.
3. Define Arithmetic Expressions
Keep arithmetic outside the FSM itself. Each small expression accepts two floats and implements exactly one operation.
CalcAdd
input: a:Float & b:Float
output: r:Float
logic
r = a + b
CalcSub
input: a:Float & b:Float
output: r:Float
logic
r = a - b
Multiplication follows the same pattern. Plain division protects itself from division by zero by returning zero.
A second family of expressions returns CalcResult. This lets
the caller distinguish a real zero from an invalid calculation.
CalcWithValidationDiv
input: a:Float & b:Float
output: r:CalcResult
logic
if b==0 then
r.result = 0
r.isValid = false
else
r.result = a / b
r.isValid = true
end
GenerateInvalidCalcResult provides the same explicit invalid
result when no operation has been selected yet.
4. Keep a Small Piece of In-Memory Context
The state name already controls behavior. The context stores a friendly description so callers can ask which mode is active.
@context
CalcFsmContext
mode: String (default=no operation)
This context is used by an in-memory FSM, so it is transient:
it belongs to the running instance and is not stored in a database. A
restart begins again from the FSM's default state and context value.
5. Define the In-Memory FSM
Now we can define the central concept. Adding in-memory after
the FSM name selects transient state handling for this machine.
@fsm
CalcFSM in-memory
states: NoOp, Adding, Subtracting, Multiplying, Dividing
default: NoOp
states lists every possible mode. default chooses
where a new FSM starts. Here, NoOp prevents calculations from
accidentally using an operation before one is selected.
Because setOperation is handled by Every, each
operation can be selected from any current state. An unsupported value
returns the machine to NoOp.
Declare typed events
Events are the FSM's public operations. Their inputs and outputs match the API methods that will connect to them later.
event getMode in(_) out(mode:String)
event setOperation in(op:String) out(result:String)
event calculate in(a:Float & b:Float) out(r:Float)
event calculateWithValidation in(a:Float & b:Float) out(r:CalcResult)
Make context and expressions available
The FSM imports its context with an alias and does the same for several
expressions. Aliases such as add and divV keep
the state handlers compact.
use context CalcFsmContext as fsmCtx
use expression CalcAdd as add
use expression CalcSub as sub
use expression CalcMul as mul
use expression CalcDiv as div
use expression CalcWithValidationAdd as addV
use expression CalcWithValidationSub as subV
use expression CalcWithValidationMul as mulV
use expression CalcWithValidationDiv as divV
use expression GenerateInvalidCalcResult
6. Add Shared Transitions and State Behavior
Handle mode changes from every state
Every is a shared state section. Its handlers are available
regardless of which concrete state is active, so we only need to define
setOperation and getMode once.
state Every
on event setOperation: logic
if op == "add" then
next Adding: result = "ADD"
end
if op == "sub" then
next Subtracting: result = "SUB"
end
if op == "mul" then
next Multiplying: result = "MUL"
end
if op == "div" then
next Dividing: result = "DIV"
end
next NoOp
on event getMode: logic
emit mode = fsmCtx.mode
next changes the active state and can also assign an event
output. emit returns a value without changing state. If no
supported operation matches, the final next NoOp provides a
safe fallback.
Use lifecycle handlers
A state's on enter handler runs when that state becomes
active. Here it updates the friendly mode stored in context.
state Adding
on enter:
update fsmCtx.mode = "Adding mode."
on event calculate:
emit r = add(a, b)
on event calculateWithValidation: logic
emit r = addV(a, b)
on exit: placeholder
This one state demonstrates the main FSM actions:
update changes context, and emit returns the
expression result. A placeholder exit marks a lifecycle hook
without adding behavior yet.
Give each state its own calculation
Subtracting, Multiplying, and Dividing keep the same event contract but
invoke different expressions. A handler may call an imported alias such
as sub(a, b), or the expression name directly, as the
multiplying and dividing handlers demonstrate.
state Dividing
on enter:
update fsmCtx.mode = "Dividing mode"
on event calculate:
emit r = CalcDiv(a, b)
on event calculateWithValidation:
emit r = CalcWithValidationDiv(a, b)
Keep the default state safe
state NoOp
on enter: logic
update fsmCtx.mode = "No-operation mode. Returns zero"
on event calculate: logic
emit r = 0
on event calculateWithValidation: logic
emit r = GenerateInvalidCalcResult()
on exit: empty
empty explicitly says that no exit work is needed. The source
also demonstrates an external exit hook on the Subtracting
state; that hook delegates lifecycle work to a named method in a source
file and is separate from the calculator's main learning flow.
7. Connect the API to the FSM
The service does not choose arithmetic behavior itself. It implements the API, imports one FSM instance, and forwards each API method to the matching typed event.
@service
CalcService
use config CalcConfig as myCfg
impl api CalcApi as api on myCfg.apiConfig.port
use fsm CalcFSM as fsm
connect api.getMode -> fsm.getMode
connect api.setOperation -> fsm.setOperation
connect api.calculate -> fsm.calculate
connect api.calculateWithValidation -> fsm.calculateWithValidation
This is an important separation: the service owns integration, the FSM owns state and dispatch, and expressions own the arithmetic logic.
8. Configure the Service and UI
This example needs no broker or database configuration. The service only needs an API port and logging, while the UI gets its own port and logging settings.
@config
@import config O.log.config@1.0.0 as LogConfig
CalcConfig
apiConfig: ApiConfig
logConfig: LogConfig
ApiConfig
port: Int (default=8080)
UiConfig
port: Int (default=8080)
logConfig: LogConfig
9. Build a Dashboard for Exploring State
The dashboard deliberately separates mode selection from calculation. That lets you change the FSM once and then observe how the same inputs produce different results in different states.
@dashboard
Widget OpForm of type Form
title = ⚙️ Choose Operation!
fields:
- operation String
buttons:
submit: Choose
Widget ModeButton of type Button
label = Which Operation?
Two calculation forms accept a and b. One returns
a plain float; the other returns CalcResult so you can inspect
its validity flag.
10. Connect UI Actions to FSM-Backed APIs
The UI uses only CalcApi. It does not need to know that an FSM
sits behind the service or that context holds the readable mode.
use dashboard CalcDashboard as cal
use api CalcApi
connect cal.OpForm.submit -> CalcApi.setOperation
connect cal.ModeButton.click -> CalcApi.getMode
connect cal.CalcForm.submit -> CalcApi.calculate
connect cal.CalcValForm.submit -> CalcApi.calculateWithValidation
Afterwards, choosing add, sub, mul, or
div will change how both calculation forms behave.
11. Deploy One Stateful Service Instance
The calculator service runs with one replica because its FSM state lives
in that process's memory. The API is exposed on port 9096.
@deploy
CalcDeploy
service CalcService
replica 1
export 9096:CalcService.api
CalcUiDeploy
service CalcUi
replica 1
export 8086:CalcUi.config
dependsOn CalcDeploy
The UI is exposed on 8086 and starts after the calculator
service. For an in-memory FSM, adding replicas would create independent
state in each instance; shared or durable state would require a
different design.
12. Run Through the State Transitions
- Validate and generate the complete example.
- Start the generated service and UI.
- Open
localhost:8086and ask for the initial mode. - Calculate before selecting an operation and inspect the safe result.
- Select
add, calculate two numbers, and check the mode. - Switch to
sub,mul, anddiv. - Try validated division with zero as the second number.
- Submit an unsupported operation and verify the return to NoOp.
The useful observation is that the calculation request never changes. Only the FSM state changes, and that state selects the expression that handles the event.
13. Conclusion
You have used a finite state machine to turn one calculator API into several state-dependent behaviors. The example stays small, but it demonstrates the complete FSM loop: receive an event, inspect or change state, update context, run state-specific logic, and emit a result.
Afterwards, you should understand how to:
- declare an
in-memoryFSM and its default state; - define typed input and output for FSM events;
- use
Everyfor behavior shared by all states; - move between states with
next; - return event results with
emit; - update readable state information in context;
- use enter and exit lifecycle hooks;
- delegate state-specific work to reusable expressions;
- connect a service API cleanly to an FSM.
Executable model
<\> Implementation
Explore the runnable model by responsibility, then select a file to inspect its complete source.
# @ocean-meta-start
# tags:
# - documentation
# perspective:
# feature: in-memory-calculator-fsm
# @ocean-meta-end
@info
name: CalcService
version: 1.0.0
description: CalcService is a minimal example service demonstrating the use of
finite state machines (FSMs) and expressions in the Ocean-lab DSL platform.
It exposes a simple API with two operations: one to set the current arithmetic
operation (`add`, `sub`, `mul`, `div`) and another to calculate the result of
two input values (`a`, `b`). Internally, the service uses a state machine to
track the selected operation and apply the appropriate expression accordingly.
This example showcases key DSL concepts including typed inputs/outputs, reusable
expressions, FSM-driven behavior, and clean API-service integration.
CalcService is a useful learning artifact and foundation for experimenting with
domain logic encapsulation using FSMs and stateless function design via DSL
expressions.
# @ocean-meta-start
# tags:
# - datatype
# perspective:
# feature: in-memory-calculator-fsm
# service: calc-service
# @ocean-meta-end
@datatype
CalcResult
result : Float
isValid : Boolean
# @ocean-meta-start
# tags:
# - calc-api
# - rest-api
# perspective:
# feature: in-memory-calculator-fsm
# service: calc-service
# @ocean-meta-end
@api
CalcApi style: rest
engine = gin
configType = ApiConfig
tags = external
version = 1.0.0
description = Calculator API
basePath = /
generateSwagger = true
get /mode getMode(_) : String
post /set-op/{op} setOperation(op:String) : String
post /calc/{a}/{b} calculate(a:Float, b:Float) : Float
post /calc-valid/{a}/{b} calculateWithValidation(a:Float, b:Float) : CalcResult
# @ocean-meta-start
# tags:
# - expression
# perspective:
# feature: in-memory-calculator-fsm
# service: calc-service
# @ocean-meta-end
@expression
CalcAdd
input : a:Float & b:Float
output : r:Float
logic
r = a + b
CalcSub
input : a:Float & b:Float
output : r:Float
logic
r = a - b
CalcMul
input : a:Float & b:Float
output : r:Float
logic
r = a * b
CalcDiv
input : a:Float & b:Float
output : r:Float
logic
if b==0 then
r = 0
else
r = a / b
end
CalcWithValidationAdd
input : a:Float & b:Float
output : r:CalcResult
logic
r.result = a + b
r.isValid = true
CalcWithValidationSub
input : a:Float & b:Float
output : r:CalcResult
logic
r.result = a - b
r.isValid = true
CalcWithValidationMul
input : a:Float & b:Float
output : r:CalcResult
logic
r.result = a * b
r.isValid = true
CalcWithValidationDiv
input : a:Float & b:Float
output : r:CalcResult
logic
if b==0 then
r.result = 0
r.isValid = false
else
r.result = a / b
r.isValid = true
end
GenerateInvalidCalcResult
input : _
output : r:CalcResult
logic
r.result = 0
r.isValid = false
Echo
input : s:String
output : e:String
logic
e = s
GetMode
input : _
output : mode:String
logic
mode = nowString()
# @ocean-meta-start
# tags:
# - context
# perspective:
# feature: in-memory-calculator-fsm
# service: calc-service
# @ocean-meta-end
@context
CalcFsmContext
mode : String (default=no operation)
# @ocean-meta-start
# tags:
# - configuration
# perspective:
# feature: in-memory-calculator-fsm
# service: all
# @ocean-meta-end
@config
@import config O.log.config@1.0.0 as LogConfig
CalcConfig
apiConfig : ApiConfig
logConfig : LogConfig
ApiConfig
port: Int (default=8080)
UiConfig
port: Int (default=8080)
logConfig : LogConfig
# @ocean-meta-start
# tags:
# - in-memory-fsm
# - fsm
# perspective:
# feature: in-memory-calculator-fsm
# service: calc-service
# @ocean-meta-end
@fsm
CalcFSM in-memory
states : NoOp, Adding, Subtracting, Multiplying, Dividing
default : NoOp
event getMode in(_) out(mode:String)
event setOperation in(op:String) out(result:String)
event calculate in(a:Float & b:Float) out(r:Float)
event calculateWithValidation in(a:Float & b:Float) out(r:CalcResult)
use context CalcFsmContext as fsmCtx
use expression CalcAdd as add
use expression CalcSub as sub
use expression CalcMul as mul
use expression CalcDiv as div
use expression CalcWithValidationAdd as addV
use expression CalcWithValidationSub as subV
use expression CalcWithValidationMul as mulV
use expression CalcWithValidationDiv as divV
use expression GenerateInvalidCalcResult
state Every
on event setOperation : logic
if op == "add" then
next Adding : result = "ADD"
end
if op == "sub" then
next Subtracting : result = "SUB"
end
if op == "mul" then
next Multiplying : result = "MUL"
end
if op == "div" then
next Dividing : result = "DIV"
end
#next NoOp : result="NOP"
next NoOp
on event getMode : logic
emit mode = fsmCtx.mode
state NoOp
on enter : logic
update fsmCtx.mode = "No-operation mode. Returns zero"
on event calculate : logic
emit r = 0
on event calculateWithValidation : logic
emit r = GenerateInvalidCalcResult()
on exit : empty
state Adding
on enter:
update fsmCtx.mode = "Adding mode."
on event calculate:
emit r = add(a, b)
on event calculateWithValidation : logic
emit r = addV(a, b)
on exit : placeholder
state Subtracting
on enter:
update fsmCtx.mode = "Subtracting mode"
on event calculate:
var res Float
res = sub(a, b)
emit r = res
on event calculateWithValidation:
var res CalcResult
res = subV(a, b)
emit r = res
on exit : external
source: my-funcs.go
method: MyFunc
state Multiplying
on enter:
update fsmCtx.mode = "Multiplying mode"
on event calculate:
emit r = CalcMul(a, b)
on event calculateWithValidation:
emit r = CalcWithValidationMul(a, b)
state Dividing
on enter:
update fsmCtx.mode = "Dividing mode"
on event calculate:
var res Float
res = CalcDiv(a, b)
emit r = res
on event calculateWithValidation:
var res CalcResult
res = CalcWithValidationDiv(a, b)
emit r = res
# @ocean-meta-start
# tags:
# - ui
# - dashboard
# perspective:
# feature: in-memory-calculator-fsm
# service: ui-service
# @ocean-meta-end
@dashboard
CalcDashboard
title: Calculate
subtitle: Choose an operation and calculate!
layout: SingleColumnLayout
Widget S2 of type Separator
size = sm
style = solid
label = No Validation!
Widget CalcForm of type Form
title = 🧮 Calculate!
fields:
- a Float
- b Float
buttons:
submit : 🧮 Calculate!
cancel : Reset
Widget S3 of type Separator
size = sm
style = solid
label = With Validation
Widget CalcValForm of type Form
title = 🧮 Calculate!
fields:
- a Float
- b Float
buttons:
submit : 🧮 Calculate!
cancel : Reset
Widget S5 of type Separator
size = md
style = solid
label = OPERATION
Widget ModeButton of type Button
label = Which Operation?
color = secondary
Widget OpForm of type Form
title = ⚙️ Choose Operation!
fields:
- operation String
buttons:
submit : Choose
AboutDashboard
title: About
subtitle: Calculate App! 🧮
layout: SingleColumnLayout
Widget AboutText of type Text
title = A simple yet comprehensive example for In-memory FSM applications
subtitle = This app uses an In-memory FSM with a Context.
content = <br><b>description:</b><br><p>CalcService is a minimal example service demonstrating the use of finite state machines (FSMs) and expressions in the Ocean-lab DSL platform.</p><p>It exposes a simple API with two operations: one to set the current arithmetic operation (`add`, `sub`, `mul`, `div`) and another to calculate the result of two input values (`a`, `b`). Internally, the service uses a state machine to track the selected operation and apply the appropriate expression accordingly.</p><p>This example showcases key DSL concepts including typed inputs/outputs, reusable expressions, FSM-driven behavior, and clean API-service integration.</p><p>CalcService is a useful learning artifact and foundation for experimenting with domain logic encapsulation using FSMs and stateless function design via DSL expressions.</p>
# @ocean-meta-start
# tags:
# - ui
# - dashboard
# perspective:
# feature: in-memory-calculator-fsm
# service: ui-service
# @ocean-meta-end
@ui
CalcUi
@perspectives: version:0.1.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: UiConfig
nav Calculate dashboard = CalcDashboard
nav About dashboard = AboutDashboard
header title = 🧮 Calculate App!
header subtitle = Calculation made simple 😊
header align = center
footer title = ⚡ Built with Ocean-lab
footer subtitle = Version 1.0.0
footer align = center
use dashboard CalcDashboard as cal
use dashboard AboutDashboard as about
use api CalcApi
connect cal.CalcForm.submit -> CalcApi.calculate
connect cal.CalcValForm.submit -> CalcApi.calculateWithValidation
connect cal.ModeButton.click -> CalcApi.getMode
connect cal.OpForm.submit -> CalcApi.setOperation
# @ocean-meta-start
# tags:
# - calc-api
# - service
# perspective:
# feature: in-memory-calculator-fsm
# service: calc-service
# @ocean-meta-end
@service
CalcService
@perspectives: version:0.1.0, lifestyle:stable
use config CalcConfig as myCfg
impl api CalcApi as api on myCfg.apiConfig.port
use fsm CalcFSM as fsm
connect api.getMode -> fsm.getMode
connect api.setOperation -> fsm.setOperation
connect api.calculate -> fsm.calculate
connect api.calculateWithValidation -> fsm.calculateWithValidation
# @ocean-meta-start
# tags:
# - deployment
# perspective:
# feature: in-memory-calculator-fsm
# service: all
# @ocean-meta-end
@deploy
Name: calc-deploy
CalcDeploy
service CalcService
replica 1
export 9096:CalcService.api
CalcUiDeploy
service CalcUi
replica 1
export 8086:CalcUi.config
dependsOn CalcDeploy
flowchart
u[User]
subgraph sys[System]
subgraph s[CalcService]
a[API]
f[FSM]
e[Expressions]
end
end
u -.communicates.-> a
a --events--> f
f -.uses.-> e