Packages

Define pure functional cores in target-agnostic Gleam, which can wrapped to run on both the Erlang and JS targets.

Current section

Files

Jump to
drift src drift.erl
Raw

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(EMP, EMQ) :: {context,
integer(),
drift@internal@timer:timers(EMP),
list(EMQ)}.
-opaque step(EMR, EMS, EMT, EMU) :: {continue_step, context(EMS, EMT), EMR} |
{stop_step, list(EMT), EMR} |
{stop_step_with_error, list(EMT), EMU}.
-opaque continuation(EMV, EMW, EMX, EMY, EMZ) :: {continuation,
integer(),
fun((context(EMX, EMY), EMW, EMV) -> step(EMW, EMX, EMY, EMZ))}.
-opaque stepper(ENA, ENB) :: {stepper, ENA, drift@internal@timer:timers(ENB)}.
-type next(ENC, END, ENE, ENF) :: {continue,
list(ENE),
stepper(ENC, END),
gleam@option:option(integer())} |
{stop, list(ENE), ENC} |
{stop_with_error, list(ENE), ENF}.
-opaque effect_context(ENG) :: {effect_context, ENG}.
-opaque effect(ENH) :: {effect, integer(), fun((ENH) -> nil)}.
-type action(ENI) :: {action, effect(ENI), ENI}.
-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(ENN, ENO), integer(), ENN) -> {context(ENN, ENO),
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(ENT, ENU), drift@internal@timer:timer()) -> {context(ENT, ENU),
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(ENZ, EOA)) -> context(ENZ, EOA).
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(EOF, EOG), EOG) -> context(EOF, EOG).
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(EOL, EOM), list(EOM)) -> context(EOL, EOM).
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(EOS, EOT), gleam@option:option(EOT)) -> context(EOS, EOT).
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(EPI, EPJ, EPK, EPL),
fun((context(EPJ, EPK), EPI) -> step(EPI, EPJ, EPK, EPL))
) -> step(EPI, EPJ, EPK, EPL).
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(EQA, EQB), EQE) -> step(EQE, EQA, EQB, 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(EQK, EQL), EQO) -> step(EQO, EQK, EQL, 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(EQU, EQV), EQY) -> step(any(), EQU, EQV, EQY).
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(ERE, ERF),
ERI,
fun((continuation(ERJ, ERI, ERE, ERF, ERK)) -> ERF),
fun((context(ERE, ERF), ERI, ERJ) -> step(ERI, ERE, ERF, ERK))
) -> step(ERI, ERE, ERF, ERK).
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(ESA, ESB), ESE, continuation(ESF, ESE, ESA, ESB, ESG), ESF) -> step(ESE, ESA, ESB, ESG).
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(ETA, ETB) -> {stepper(ETA, any()), effect_context(ETB)}.
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(EUM, EUN, EUO, EUP)) -> next(EUM, EUN, EUO, EUP).
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(ETG, ETH),
integer(),
fun((context(ETH, ETK), ETG, ETH) -> step(ETG, ETH, ETK, ETN))
) -> next(ETG, ETH, ETK, ETN).
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(ETW, ETX),
integer(),
ETX,
fun((context(ETX, EUA), ETW, ETX) -> step(ETW, ETX, EUA, EUD))
) -> next(ETW, ETX, EUA, EUD).
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(EUY), fun((EUY) -> EUY)) -> effect_context(EUY).
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(EVB)) -> EVB.
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((EVD) -> nil)) -> effect(EVD).
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(EVF), EVF) -> action(EVF).
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(EOZ, EPA), fun((action(EPD)) -> EPA), effect(EPD), EPD) -> context(EOZ, EPA).
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(EVI), action(any())) -> effect_context(EVI).
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().