don't re-compute values on first render (unnecessary, values are part of state)

This commit is contained in:
Joeri Exelmans 2025-05-14 06:46:03 +02:00
parent 174bab79e4
commit 2d0deca127
14 changed files with 274 additions and 115 deletions

View file

@ -55,3 +55,8 @@ footer a {
background-color: dodgerblue; background-color: dodgerblue;
color: white; color: white;
} }
.factoryReset {
background-color: red;
color: black;
}

View file

@ -1,16 +1,47 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import './App.css' import './App.css'
import { Editor, type EditorState } from './Editor' import { Editor, type EditorState } from './Editor'
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations"; import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
import { CommandContext } from './CommandContext'; import { CommandContext } from './CommandContext';
import { EnvContext } from './EnvContext'; import { deserialize, serialize } from './types';
import { extendedEnv } from './EnvContext';
import { useEffectBetter } from './util/use_effect_better';
const commands: [string, string[], string][] = [
["call" , ['c' ], "call" ],
["eval" , ['e','Tab','Enter'], "eval" ],
["transform", ['t', '.' ], "transform" ],
["let" , ['l', '=', 'a' ], "let ... in ..."],
];
const examples: [string, EditorState][] = [
["empty editor", initialEditorState],
["push to list", nonEmptyEditorState],
["function w/ 4 params", tripleFunctionCallEditorState]];
export function App() { export function App() {
// const [history, setHistory] = useState([initialEditorState]); // const [history, setHistory] = useState([initialEditorState]);
// const [history, setHistory] = useState([nonEmptyEditorState]); // const [history, setHistory] = useState([nonEmptyEditorState]);
const [history, setHistory] = useState([tripleFunctionCallEditorState]); // const [history, setHistory] = useState([tripleFunctionCallEditorState]);
// const [future, setFuture] = useState<EditorState[]>([]);
const [future, setFuture] = useState<EditorState[]>([]); // load from localStorage
const [history, setHistory] = useState<EditorState[]>(
localStorage["history"]
? JSON.parse(localStorage["history"]).map(s => deserialize(s, extendedEnv))
: [initialEditorState]
);
const [future, setFuture] = useState<EditorState[]>(
localStorage["future"]
? JSON.parse(localStorage["future"]).map(s => deserialize(s, extendedEnv))
: []
);
useEffectBetter(() => {
// persist accross reloads
localStorage["history"] = JSON.stringify(history.map(serialize));
localStorage["future"] = JSON.stringify(future.map(serialize));
}, [history, future]);
const pushHistory = (callback: (p: EditorState) => EditorState) => { const pushHistory = (callback: (p: EditorState) => EditorState) => {
const newState = callback(history.at(-1)!); const newState = callback(history.at(-1)!);
@ -51,12 +82,6 @@ export function App() {
window.onkeydown = onKeyDown; window.onkeydown = onKeyDown;
}, []); }, []);
const commands: [string, string[], string][] = [
["call" , ['c' ], "call" ],
["eval" , ['e','Tab','Enter' ], "eval" ],
["transform", ['t', '.' ], "transform" ],
["let" , ['l', '=', 'a' ], "let ... in ..."],
];
const [highlighted, setHighlighted] = useState( const [highlighted, setHighlighted] = useState(
commands.map(() => false)); commands.map(() => false));
@ -68,12 +93,16 @@ export function App() {
}]; }];
})); }));
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
// @ts-ignore
pushHistory(_ => examples[e.target.value][1]);
}
return ( return (
<> <>
<header> <header>
<button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) [Ctrl+Z]</button> <button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) [Ctrl+Shift+Z]</button> <button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></button>
Commands:
{ {
commands.map(([_, keys, descr], i) => commands.map(([_, keys, descr], i) =>
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}> <span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
@ -81,6 +110,17 @@ export function App() {
{descr} {descr}
</span>) </span>)
} }
<select onClick={onSelectExample}>
{
examples.map(([name], i) => {
return <option key={i} value={i}>{name}</option>;
})
}
</select>
<button className="factoryReset" onClick={() => {
setHistory(_ => [initialEditorState]);
setFuture(_ => []);
}}>FACTORY RESET</button>
</header> </header>
<main onKeyDown={onKeyDown}> <main onKeyDown={onKeyDown}>

View file

@ -3,6 +3,7 @@
display: inline-block; display: inline-block;
margin: 4px; margin: 4px;
color: black; color: black;
background-color: white;
} }
.functionName { .functionName {

View file

@ -2,21 +2,11 @@ import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction,
import { Editor, type EditorState } from "./Editor"; import { Editor, type EditorState } from "./Editor";
import { Value } from "./Value"; import { Value } from "./Value";
import type { Dynamic, SetStateFn, State2Props } from "./util/extra"; import { DeepError, type ResolvedType, type SetStateFn, type State2Props } from "./types";
import { useEffect } from "react"; import { useEffect } from "react";
import "./CallBlock.css"; import "./CallBlock.css";
import { useEffectBetter } from "./util/use_effect_better";
export class DeepError {
e: Error;
depth: number;
constructor(e, depth) {
this.e = e;
this.depth = depth;
}
};
type ResolvedType = Dynamic | DeepError | undefined;
export interface CallBlockState< export interface CallBlockState<
FnState=EditorState, FnState=EditorState,
@ -34,6 +24,26 @@ interface CallBlockProps<
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> { > extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
} }
export function resolveCallBlock(fn: ResolvedType, input: ResolvedType) {
if (have(input) && have(fn)) {
try {
const outputResolved = apply(input)(fn); // may throw
return outputResolved; // success
}
catch (e) {
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
throw e;
}
return new DeepError(e, 0); // eval error
}
}
else if (input instanceof DeepError) {
return input; // bubble up the error
}
else if (fn instanceof DeepError) {
return new DeepError(fn.e, fn.depth+1);
}
}
function have(resolved: ResolvedType) { function have(resolved: ResolvedType) {
return resolved && !(resolved instanceof DeepError); return resolved && !(resolved instanceof DeepError);
@ -50,35 +60,13 @@ function headlessCallBlock({state, setState}: CallBlockProps) {
const setResolved = (callback: SetStateFn<ResolvedType>) => { const setResolved = (callback: SetStateFn<ResolvedType>) => {
setState(state => ({...state, resolved: callback(state.resolved)})); setState(state => ({...state, resolved: callback(state.resolved)}));
} }
useEffect(() => {
useEffectBetter(() => {
// Here we do something spooky: we update the state in response to state change... // Here we do something spooky: we update the state in response to state change...
// The reason this shouldn't give problems is because we update the state in such a way that the changes only 'trickle up', rather than getting stuck in a cycle. // The reason this shouldn't give problems is because we update the state in such a way that the changes only 'trickle up', rather than getting stuck in a cycle.
if (have(input.resolved) && have(fn.resolved)) { setResolved(() => resolveCallBlock(fn.resolved, input.resolved));
try { }, [fn.resolved, input.resolved]);
const outputResolved = apply(input.resolved)(fn.resolved); // may throw
setResolved(() => outputResolved); // success
}
catch (e) {
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
throw e;
}
setResolved(() => new DeepError(e, 0)); // eval error
}
}
else if (input.resolved instanceof DeepError) {
setResolved(() => input.resolved); // bubble up the error
}
else if (fn.resolved instanceof DeepError) {
setResolved(() => {
// @ts-ignore
return new DeepError(fn.resolved.e, fn.resolved.depth+1);
}); // bubble up the error
}
else {
// no errors and at least one is undefined:
setResolved(() => undefined); // chill out
}
}, [input.resolved, fn.resolved]);
const onFnCancel = () => { const onFnCancel = () => {
setState(state => state.input); // we become our input setState(state => state.input); // we become our input
} }
@ -109,7 +97,9 @@ export function CallBlock({ state, setState }: CallBlockProps) {
/> />
{/* Output (or Error) */} {/* Output (or Error) */}
{ state.resolved instanceof DeepError && state.resolved.e.toString() { state.resolved instanceof DeepError && state.resolved.e.toString()
|| state.resolved && <><Value dynamic={state.resolved} />&#x2611;</>} || state.resolved && <><Value dynamic={state.resolved} />
{/* &#x2611; */}
</>}
</div> </div>
</div> </div>
</span>; </span>;

View file

@ -1,10 +1,10 @@
import { getSymbol, getType, symbolFunction } from "dope2"; import { getSymbol, getType, symbolFunction } from "dope2";
import { useContext, useEffect, useReducer, useRef, useState } from "react"; import { useContext, useEffect, useReducer, useRef, useState } from "react";
import { CallBlock, DeepError, type CallBlockState } from "./CallBlock"; import { CallBlock, type CallBlockState } from "./CallBlock";
import { InputBlock, type InputBlockState } from "./InputBlock"; import { InputBlock, type InputBlockState } from "./InputBlock";
import { Type } from "./Type"; import { Type } from "./Type";
import { type Dynamic, type State2Props } from "./util/extra"; import { DeepError, type Dynamic, type State2Props } from "./types";
import "./Editor.css"; import "./Editor.css";
import { LetInBlock, type LetInBlockState } from "./LetInBlock"; import { LetInBlock, type LetInBlockState } from "./LetInBlock";

View file

@ -6,7 +6,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { Type } from "./Type"; import { Type } from "./Type";
import "./InputBlock.css"; import "./InputBlock.css";
import type { Dynamic, State2Props } from "./util/extra"; import type { Dynamic, State2Props } from "./types";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
export interface InputBlockState { export interface InputBlockState {
@ -30,7 +30,7 @@ const computeSuggestions = (text, env, filter) => {
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []), ... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
... trie.suggest(env.name2dyn)(text)(Infinity), ... trie.suggest(env.name2dyn)(text)(Infinity),
] ]
return ls; // return ls;
return [ return [
...ls.filter(filter), // ones that match filter come first ...ls.filter(filter), // ones that match filter come first
...ls.filter(s => !filter(s)), ...ls.filter(s => !filter(s)),
@ -158,7 +158,7 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
spellCheck={false}/> spellCheck={false}/>
{/* Single 'grey' suggestion */} {/* Single 'grey' suggestion */}
<span className="text-block suggest">{singleSuggestion}</span> <span className="text-block suggest">{singleSuggestion}</span>
{ resolved && <>&#x2611;</>} {/* { resolved && <>&#x2611;</>} */}
</span> </span>
</span>; </span>;
} }

View file

@ -1,11 +1,10 @@
import type { EditorState } from "./Editor"; import type { EditorState } from "./Editor";
import type { Dynamic } from "./util/extra"; import type { Dynamic, ResolvedType } from "./types";
export interface LambdaBlockState { export interface LambdaBlockState {
kind: "lambda"; kind: "lambda";
env: any;
paramName: string; paramName: string;
expr: EditorState; expr: EditorState;
resolved: undefined | Dynamic; resolved: ResolvedType;
} }

View file

@ -1,31 +1,39 @@
import { useContext, useEffect, useRef } from "react"; import { useContext, useEffect, useRef } from "react";
import { Editor, type EditorState } from "./Editor"; import { Editor, type EditorState } from "./Editor";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
import type { Dynamic, State2Props } from "./util/extra"; import { DeepError, type ResolvedType, type State2Props } from "./types";
import { growEnv } from "dope2"; import { growEnv } from "dope2";
import { autoInputWidth } from "./util/dom_trickery"; import { autoInputWidth } from "./util/dom_trickery";
import "./LetInBlock.css"; import "./LetInBlock.css";
import { useEffectBetter } from "./util/use_effect_better";
export interface LetInBlockState { export interface LetInBlockState {
kind: "let"; kind: "let";
name: string; name: string;
value: EditorState; value: EditorState;
inner: EditorState; inner: EditorState;
resolved: undefined | Dynamic; resolved: ResolvedType;
} }
interface LetInBlockProps extends State2Props<LetInBlockState> { interface LetInBlockProps extends State2Props<LetInBlockState> {
} }
export function makeInnerEnv(env, name: string, value: ResolvedType) {
if (value && !(value instanceof DeepError)) {
return growEnv(env)(name)(value)
}
return env;
}
export function LetInBlock({state, setState}: LetInBlockProps) { export function LetInBlock({state, setState}: LetInBlockProps) {
const {name, value, inner} = state; const {name, value, inner} = state;
const env = useContext(EnvContext); const env = useContext(EnvContext);
const innerEnv = makeInnerEnv(env, name, value.resolved);
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
const setInner = inner => setState(state => ({...state, inner})); const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
const setValue = value => setState(state => ({...state, value})); const setValue = callback => setState(state => ({...state, value: callback(state.value)}));
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => { const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
setState(state => ({...state, name: e.target.value})); setState(state => ({...state, name: e.target.value}));
@ -35,10 +43,14 @@ export function LetInBlock({state, setState}: LetInBlockProps) {
nameRef.current?.focus(); nameRef.current?.focus();
}, []); }, []);
useEffect(() => autoInputWidth(nameRef, name), [nameRef, name]); useEffectBetter(() => {
// bubble up
setState(state => ({...state, resolved: inner.resolved}));
}, [inner.resolved])
useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]);
const innerEnv = (name !== '') && value.resolved
&& growEnv(env)(name)(value.resolved) || env;
return <span className="letIn"> return <span className="letIn">
<div className="decl"> <div className="decl">
<span className="keyword">let</span> <span className="keyword">let</span>
@ -46,7 +58,7 @@ export function LetInBlock({state, setState}: LetInBlockProps) {
ref={nameRef} ref={nameRef}
className='editable' className='editable'
value={name} value={name}
placeholder="<variable name>" placeholder="<name>"
onChange={onChangeName} onChange={onChangeName}
/> />
<span className="keyword">=</span> <span className="keyword">=</span>

View file

@ -1,4 +1,4 @@
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool} from "dope2"; import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set} from "dope2";
import "./Value.css"; import "./Value.css";
@ -19,21 +19,14 @@ export function Value({dynamic}) {
switch (symbol) { switch (symbol) {
case symbolFunction: case symbolFunction:
return <ValueFunction/>; return <ValueFunction/>;
// return <BinaryType type={type} cssClass="functionType" infix="&rarr;" prefix="" suffix=""/>;
// case symbolProduct:
// return <BinaryType type={type} cssClass="productType" infix="&#10799;" prefix="" suffix=""/>;
case symbolSum: case symbolSum:
return <ValueSum val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>; return <ValueSum val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>;
case symbolProduct: case symbolProduct:
return <ValueProduct val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>; return <ValueProduct val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>;
case symbolDict: case symbolDict:
return <ValueDict val={inst} keyType={type.params[0](type)} valueType={type.params[1](type)}/>; return <ValueDict val={inst} keyType={type.params[0](type)} valueType={type.params[1](type)}/>;
case symbolSet:
// return <BinaryType type={type} cssClass="dictType" infix="&rArr;" prefix="{" suffix="}"/>; return <ValueSet val={inst} elemType={type.params[0](type)} />;
// case symbolSet:
// return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
case symbolList: case symbolList:
return <ValueList val={inst} elemType={type.params[0](type)} />; return <ValueList val={inst} elemType={type.params[0](type)} />;
@ -60,6 +53,15 @@ function ValueBool({val}) {
function ValueList({val, elemType}) { function ValueList({val, elemType}) {
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>; return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
} }
function ValueSet({val, elemType}) {
return <span className="setType">{'{'}{set.fold(acc => elem => acc.concat([elem]))([])(val).map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}{'}'}</span>;
}
function ValueDict({val, keyType, valueType}) {
return <span className="dictType">{'{'}{set.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => <span key={i}>
<Value key={i} dynamic={{i:key, t:keyType}}/>
<Value key={i} dynamic={{i:value, t:valueType}}/>
</span>)}{'}'}</span>;
}
function ValueSum({val, leftType, rightType}) { function ValueSum({val, leftType, rightType}) {
return match(val) return match(val)
(l => <span className="sumType">L <Value dynamic={{i:l, t:leftType}}/></span>) (l => <span className="sumType">L <Value dynamic={{i:l, t:leftType}}/></span>)
@ -68,23 +70,23 @@ function ValueSum({val, leftType, rightType}) {
function ValueProduct({val, leftType, rightType}) { function ValueProduct({val, leftType, rightType}) {
return <span className="productType">(<Value dynamic={{i:getLeft(val), t:leftType}}/>,&nbsp;<Value dynamic={{i:getRight(val), t:rightType}} />)</span>; return <span className="productType">(<Value dynamic={{i:getLeft(val), t:leftType}}/>,&nbsp;<Value dynamic={{i:getRight(val), t:rightType}} />)</span>;
} }
function ValueDict({val, keyType, valueType}) { // function ValueDict({val, keyType, valueType}) {
let i=0; // let i=0;
return <span className="dictType">{'{'}<>{ // return <span className="dictType">{'{'}<>{
dict.fold // dict.fold
(acc => key => value => { // (acc => key => value => {
console.log({acc, key, value}); // console.log({acc, key, value});
return acc.concat([<> // return acc.concat([<>
<Value key={i++} dynamic={{i: key, t: keyType}}/> // <Value key={i++} dynamic={{i: key, t: keyType}}/>
&rArr; // &rArr;
<Value key={i++} dynamic={{i: value, t: valueType}}/> // <Value key={i++} dynamic={{i: value, t: valueType}}/>
</>]); // </>]);
}) // })
([]) // ([])
(val) // (val)
.map(result => { // .map(result => {
console.log(result); // console.log(result);
return result; // return result;
}) // })
}</>{'}'}</span>; // }</>{'}'}</span>;
} // }

View file

@ -93,4 +93,3 @@ export const tripleFunctionCallEditorState: EditorState = {
}, },
resolved: apply(fourtyFive)(apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith4Params)))), resolved: apply(fourtyFive)(apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith4Params)))),
}; };

108
src/types.ts Normal file
View file

@ -0,0 +1,108 @@
import { Double, getDefaultTypeParser, Int, prettyT, trie } from "dope2";
import type { EditorState } from "./Editor";
import { resolveCallBlock } from "./CallBlock";
import { makeInnerEnv } from "./LetInBlock";
import { parseDouble, parseInt } from "./util/parse";
export interface Dynamic {
i: any;
t: any;
}
export type SetStateFn<InType=EditorState,OutType=InType> = (state: InType) => OutType;
export interface State2Props<InType,OutType=InType> {
state: InType;
setState: (callback: SetStateFn<InType,OutType>) => void;
}
export class DeepError {
e: Error;
depth: number;
constructor(e, depth) {
this.e = e;
this.depth = depth;
}
};
export type ResolvedType = Dynamic | DeepError | undefined;
export const serialize = (s: EditorState) => {
if (s.kind === "input") {
return {
...s,
// dirty: we write out the type in case the value is a literal and needs to be parsed
type: s.resolved && prettyT(s.resolved.t),
resolved: undefined,
};
}
if (s.kind === "call") {
return {
...s,
fn: serialize(s.fn),
input: serialize(s.input),
resolved: undefined,
};
}
if (s.kind === "let") {
return {
...s,
value: serialize(s.value),
inner: serialize(s.inner),
resolved: undefined,
};
}
if (s.kind === "lambda") {
return {
...s,
expr: serialize(s.expr),
resolved: undefined,
}
}
};
function parseLiteral(text: string, type: string) {
// dirty
if (type === "Int") {
return {i: parseInt(text), t: Int};
}
if (type === "Double") {
return {i: parseDouble(text), t: Double};
}
}
export const deserialize = (s, env) => {
if (s.kind === "input") {
return {
...s,
resolved: trie.get(env.name2dyn)(s.text) || parseLiteral(s.text, s.type),
};
}
if (s.kind === "call") {
const fn = deserialize(s.fn, env);
const input = deserialize(s.input, env);
return {
...s,
fn,
input,
resolved: resolveCallBlock(fn.resolved, input.resolved),
};
}
if (s.kind === "let") {
const value = deserialize(s.value, env);
const inner = deserialize(s.inner, makeInnerEnv(env, s.name, value.resolved));
return {
...s,
value,
inner,
resolved: inner.resolved,
};
}
if (s.kind === "lambda") {
return {
...s,
expr: deserialize(s.expr, env),
resolved: undefined,
}
}
};

View file

@ -49,8 +49,8 @@ export function focusPrevElement() {
} }
} }
export const autoInputWidth = (inputRef: React.RefObject<HTMLInputElement| null>, text) => { export const autoInputWidth = (inputRef: React.RefObject<HTMLInputElement| null>, text, emptyWidth=150) => {
if (inputRef.current) { if (inputRef.current) {
inputRef.current.style.width = `${text.length === 0 ? 150 : (text.length*8.7)}px`; inputRef.current.style.width = `${text.length === 0 ? emptyWidth : (text.length*8.7)}px`;
} }
} }

View file

@ -1,13 +0,0 @@
import type { EditorState } from "../Editor";
export interface Dynamic {
i: any;
t: any;
}
export type SetStateFn<InType=EditorState,OutType=InType> = (state: InType) => OutType;
export interface State2Props<InType,OutType=InType> {
state: InType;
setState: (callback: SetStateFn<InType,OutType>) => void;
}

View file

@ -0,0 +1,16 @@
import { useEffect, useRef } from "react"
// like useEffect, but doesn't run on first render
export const useEffectBetter = (callback, deps) => {
// detect development mode, where render function is always called twice:
const firstRender = useRef(import.meta.env.MODE === "development" ? 2 : 1);
useEffect(() => {
if (firstRender.current > 0) {
firstRender.current -= 1;
}
else {
callback();
}
}, deps);
}