-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().