Current section
Files
Jump to
Current section
Files
src/drift.erl
-module(drift).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/drift.gleam").
-export([now/1, start_timer/3, cancel_timer/2, cancel_all_timers/1, output/2, output_many/2, output_option/2, chain/2, continue/2, stop/2, stop_with_error/2, await/4, resume/4, continuation_id/1, new/2, tick/3, step/4, use_effect_context/2, read_effect_context/1, new_effect/1, bind_effect/2, perform/4, perform_effect/2, effect_id/1, reset_ids/0]).
-export_type([cancelled/0, context/2, step/4, continuation/5, stepper/2, next/4, effect_context/1, effect/1, action/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Define pure functional cores, which can be wrapped to handle side effects.\n"
" The state of the core is represented with a `Stepper`, which for each step,\n"
" handles an input and produces a new state and outputs.\n"
).
-type cancelled() :: timer_not_found | {cancelled, integer()}.
-opaque context(EWE, EWF) :: {context,
integer(),
drift@internal@timer:timers(EWE),
list(EWF)}.
-opaque step(EWG, EWH, EWI, EWJ) :: {continue_step, context(EWH, EWI), EWG} |
{stop_step, list(EWI), EWG} |
{stop_step_with_error, list(EWI), EWJ}.
-opaque continuation(EWK, EWL, EWM, EWN, EWO) :: {continuation,
integer(),
fun((context(EWM, EWN), EWL, EWK) -> step(EWL, EWM, EWN, EWO))}.
-opaque stepper(EWP, EWQ) :: {stepper, EWP, drift@internal@timer:timers(EWQ)}.
-type next(EWR, EWS, EWT, EWU) :: {continue,
list(EWT),
stepper(EWR, EWS),
gleam@option:option(integer())} |
{stop, list(EWT), EWR} |
{stop_with_error, list(EWT), EWU}.
-opaque effect_context(EWV) :: {effect_context, EWV}.
-opaque effect(EWW) :: {effect, integer(), fun((EWW) -> nil)}.
-type action(EWX) :: {action, effect(EWX), EWX}.
-file("src/drift.gleam", 37).
?DOC(
" Gets the current timestamp from the context.\n"
" The timestamp is always the start time of the step\n"
" (time does not advance during a step, in order to keep things pure).\n"
).
-spec now(context(any(), any())) -> integer().
now(Context) ->
erlang:element(2, Context).
-file("src/drift.gleam", 42).
?DOC(" Returns a new context with a timer added to handle an input after a delay.\n").
-spec start_timer(context(EXC, EXD), integer(), EXC) -> {context(EXC, EXD),
drift@internal@timer:timer()}.
start_timer(Context, Delay, Input) ->
{Timers, Timer} = drift@internal@timer:add(
erlang:element(3, Context),
erlang:element(2, Context) + Delay,
Input
),
{{context, erlang:element(2, Context), Timers, erlang:element(4, Context)},
Timer}.
-file("src/drift.gleam", 53).
?DOC(" Returns a new context with the given timer canceled.\n").
-spec cancel_timer(context(EXI, EXJ), drift@internal@timer:timer()) -> {context(EXI, EXJ),
cancelled()}.
cancel_timer(Context, To_cancel) ->
{Timers, Cancelled} = drift@internal@timer:cancel(
erlang:element(3, Context),
erlang:element(2, Context),
To_cancel
),
{{context, erlang:element(2, Context), Timers, erlang:element(4, Context)},
case Cancelled of
none ->
timer_not_found;
{some, Time_remaining} ->
{cancelled, Time_remaining}
end}.
-file("src/drift.gleam", 67).
?DOC(" Returns a new context with all timers canceled.\n").
-spec cancel_all_timers(context(EXO, EXP)) -> context(EXO, EXP).
cancel_all_timers(Context) ->
{context,
erlang:element(2, Context),
drift@internal@timer:cancel_all(erlang:element(3, Context)),
erlang:element(4, Context)}.
-file("src/drift.gleam", 72).
?DOC(" Returns a new context with the given output added.\n").
-spec output(context(EXU, EXV), EXV) -> context(EXU, EXV).
output(Context, Output) ->
{context,
erlang:element(2, Context),
erlang:element(3, Context),
[Output | erlang:element(4, Context)]}.
-file("src/drift.gleam", 77).
?DOC(" Returns a new context with the given outputs added.\n").
-spec output_many(context(EYA, EYB), list(EYB)) -> context(EYA, EYB).
output_many(Context, Outputs) ->
gleam@list:fold(Outputs, Context, fun output/2).
-file("src/drift.gleam", 82).
?DOC(" Returns a new context with the given output added, if it was `Some`\n").
-spec output_option(context(EYH, EYI), gleam@option:option(EYI)) -> context(EYH, EYI).
output_option(Context, Optional_output) ->
case Optional_output of
none ->
Context;
{some, Value} ->
output(Context, Value)
end.
-file("src/drift.gleam", 120).
?DOC(
" If a step hasn't terminated, extracts the context and state from the step,\n"
" and returns a new step from the given function.\n"
).
-spec chain(
step(EYX, EYY, EYZ, EZA),
fun((context(EYY, EYZ), EYX) -> step(EYX, EYY, EYZ, EZA))
) -> step(EYX, EYY, EYZ, EZA).
chain(Step, F) ->
case Step of
{continue_step, Context, State} ->
F(Context, State);
_ ->
Step
end.
-file("src/drift.gleam", 132).
?DOC(
" Ends the current step, signalling to continue running the stepper.\n"
" All effects in the context should be applied by the wrapping runtime.\n"
).
-spec continue(context(EZP, EZQ), EZT) -> step(EZT, EZP, EZQ, any()).
continue(Context, State) ->
{continue_step, Context, State}.
-file("src/drift.gleam", 138).
?DOC(
" Terminates the stepper with the final state without error.\n"
" All effects in the context should still be applied by the wrapping runtime.\n"
).
-spec stop(context(EZZ, FAA), FAD) -> step(FAD, EZZ, FAA, any()).
stop(Context, State) ->
{stop_step, erlang:element(4, Context), State}.
-file("src/drift.gleam", 144).
?DOC(
" Terminates the stepper with an error.\n"
" All effects in the context should still be applied by the wrapping runtime.\n"
).
-spec stop_with_error(context(FAJ, FAK), FAN) -> step(any(), FAJ, FAK, FAN).
stop_with_error(Context, Error) ->
{stop_step_with_error, erlang:element(4, Context), Error}.
-file("src/drift.gleam", 167).
?DOC(
" Completes the current step with the given state, and adds the output\n"
" constructed by `make_output`. `continuation` will be executed with the new\n"
" context and state when it is resumed.\n"
" Designed to be used with `use`, e.g.\n"
" ```\n"
" use context, state, response <- drift.await(context, state, SomeOutput)\n"
" ```\n"
).
-spec await(
context(FAT, FAU),
FAX,
fun((continuation(FAY, FAX, FAT, FAU, FAZ)) -> FAU),
fun((context(FAT, FAU), FAX, FAY) -> step(FAX, FAT, FAU, FAZ))
) -> step(FAX, FAT, FAU, FAZ).
await(Context, State, Make_output, Continuation) ->
_pipe = Context,
_pipe@1 = output(
_pipe,
Make_output({continuation, drift_external:get_id(), Continuation})
),
continue(_pipe@1, State).
-file("src/drift.gleam", 179).
?DOC(" Resumes execution of a continuation.\n").
-spec resume(context(FBP, FBQ), FBT, continuation(FBU, FBT, FBP, FBQ, FBV), FBU) -> step(FBT, FBP, FBQ, FBV).
resume(Context, State, Continuation, Result) ->
(erlang:element(3, Continuation))(Context, State, Result).
-file("src/drift.gleam", 189).
?DOC(" Gets the id of the continuation. Should only really be needed for tests.\n").
-spec continuation_id(continuation(any(), any(), any(), any(), any())) -> integer().
continuation_id(Continuation) ->
erlang:element(2, Continuation).
-file("src/drift.gleam", 199).
?DOC(" Creates a new stepper with the given state for the pure and effectful parts.\n").
-spec new(FCP, FCQ) -> {stepper(FCP, any()), effect_context(FCQ)}.
new(State, Io_state) ->
{{stepper, State, drift@internal@timer:new()}, {effect_context, Io_state}}.
-file("src/drift.gleam", 260).
?DOC(" Ends the current step, yielding the next state.\n").
-spec end_step(step(FEB, FEC, FED, FEE)) -> next(FEB, FEC, FED, FEE).
end_step(Step) ->
case Step of
{continue_step, {context, _, Timers, Effects}, State} ->
{continue,
lists:reverse(Effects),
{stepper, State, Timers},
drift@internal@timer:next_tick(Timers)};
{stop_step_with_error, Effects@1, Error} ->
{stop_with_error, lists:reverse(Effects@1), Error};
{stop_step, Effects@2, State@1} ->
{stop, lists:reverse(Effects@2), State@1}
end.
-file("src/drift.gleam", 225).
?DOC(" Triggers all expired timers, and returns the next state of the stepper.\n").
-spec tick(
stepper(FCV, FCW),
integer(),
fun((context(FCW, FCZ), FCV, FCW) -> step(FCV, FCW, FCZ, FDC))
) -> next(FCV, FCW, FCZ, FDC).
tick(Stepper, Now, Apply) ->
{stepper, State, Timers} = Stepper,
{Timers@1, To_trigger} = drift@internal@timer:expired(Timers, Now),
_pipe = gleam@list:fold(
To_trigger,
{continue_step, {context, Now, Timers@1, []}, State},
fun(Next, Input) -> case Next of
{continue_step, Context, State@1} ->
Apply(Context, State@1, Input);
Other ->
Other
end end
),
end_step(_pipe).
-file("src/drift.gleam", 248).
?DOC(
" Applies the given input to the stepper, using the provided function.\n"
" Returns the next state of the stepper.\n"
).
-spec step(
stepper(FDL, FDM),
integer(),
FDM,
fun((context(FDM, FDP), FDL, FDM) -> step(FDL, FDM, FDP, FDS))
) -> next(FDL, FDM, FDP, FDS).
step(Stepper, Now, Input, Apply) ->
_pipe = {context, Now, erlang:element(3, Stepper), []},
_pipe@1 = Apply(_pipe, erlang:element(2, Stepper), Input),
end_step(_pipe@1).
-file("src/drift.gleam", 286).
?DOC(
" Applies a function to the state of an effect context, returning a new\n"
" effect context.\n"
).
-spec use_effect_context(effect_context(FEN), fun((FEN) -> FEN)) -> effect_context(FEN).
use_effect_context(Ctx, Fun) ->
{effect_context, Fun(erlang:element(2, Ctx))}.
-file("src/drift.gleam", 294).
?DOC(" Reads the state of an effect context.\n").
-spec read_effect_context(effect_context(FEQ)) -> FEQ.
read_effect_context(Ctx) ->
erlang:element(2, Ctx).
-file("src/drift.gleam", 318).
?DOC(
" Constructs an effect from a function to be called with a value produced later.\n"
" Each `Effect` created is unique, even if they use the same function.\n"
" This serves two purposes:\n"
" 1) Since the same side effect might be expected to be performed a specific\n"
" number of times from different contexts, treating each created effect\n"
" as unique allows discriminating between them based on equality comparison.\n"
" 2) Having a distinct id allows writing nice snapshot tests, where\n"
" effects can be identified in the output.\n"
).
-spec new_effect(fun((FES) -> nil)) -> effect(FES).
new_effect(Effect) ->
{effect, drift_external:get_id(), Effect}.
-file("src/drift.gleam", 323).
?DOC(" Binds a value to an effect, to be performed by the impure context.\n").
-spec bind_effect(effect(FEU), FEU) -> action(FEU).
bind_effect(Effect, Arg) ->
{action, Effect, Arg}.
-file("src/drift.gleam", 99).
?DOC(
" A shorthand for outputting effects to be performed.\n"
" Example:\n"
" ```\n"
" context\n"
" |> drift.perform(SomeOutput, effect, state)\n"
" |> drift.continue(state)\n"
" ```\n"
).
-spec perform(context(EYO, EYP), fun((action(EYS)) -> EYP), effect(EYS), EYS) -> context(EYO, EYP).
perform(Context, Make_output, Effect, Arg) ->
output(Context, Make_output(bind_effect(Effect, Arg))).
-file("src/drift.gleam", 328).
?DOC(" Performs a side effect that was prepared.\n").
-spec perform_effect(effect_context(FEX), action(any())) -> effect_context(FEX).
perform_effect(Ctx, Action) ->
(erlang:element(3, erlang:element(2, Action)))(erlang:element(3, Action)),
Ctx.
-file("src/drift.gleam", 337).
?DOC(" Get the id of an effect. This should only really be needed for tests.\n").
-spec effect_id(effect(any())) -> integer().
effect_id(Effect) ->
erlang:element(2, Effect).
-file("src/drift.gleam", 344).
?DOC(
" Resets the id counter used for effects and continuations,\n"
" to get deterministic ids.\n"
" Should only really be needed for tests.\n"
).
-spec reset_ids() -> nil.
reset_ids() ->
drift_external:reset_id().