Skip to content

Commit

Permalink
Merge pull request #76 from unyt-org/feat-reactive-fn
Browse files Browse the repository at this point in the history
Add reactive function decorator
  • Loading branch information
benStre authored Feb 6, 2024
2 parents ea0a8c9 + 32d6af3 commit a80ec45
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 2 deletions.
4 changes: 3 additions & 1 deletion datex_short.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { baseURL, Runtime, PrecompiledDXB, Type, Pointer, Ref, PointerProperty,

/** make decorators global */
import { assert as _assert, property as _property, sync as _sync, endpoint as _endpoint, template as _template, jsdoc as _jsdoc} from "./datex_all.ts";
import { effect as _effect, always as _always, asyncAlways as _asyncAlways, toggle as _toggle, map as _map, equals as _equals, selectProperty as _selectProperty, not as _not } from "./functions.ts";
import { effect as _effect, always as _always, reactiveFn as _reactiveFn, asyncAlways as _asyncAlways, toggle as _toggle, map as _map, equals as _equals, selectProperty as _selectProperty, not as _not } from "./functions.ts";
export * from "./functions.ts";
import { NOT_EXISTING, DX_SLOTS, SLOT_GET, SLOT_SET } from "./runtime/constants.ts";
import { AssertionError } from "./types/errors.ts";
Expand All @@ -25,6 +25,7 @@ declare global {
const endpoint: typeof _endpoint;
const always: typeof _always;
const asyncAlways: typeof _asyncAlways;
const reactiveFn: typeof _reactiveFn;
const toggle: typeof _toggle;
const map: typeof _map;
const equals: typeof _equals;
Expand Down Expand Up @@ -614,6 +615,7 @@ export function translocate<T extends Map<unknown,unknown>|Set<unknown>|Array<un
Object.defineProperty(globalThis, 'once', {value:once, configurable:false})
Object.defineProperty(globalThis, 'always', {value:_always, configurable:false})
Object.defineProperty(globalThis, 'asyncAlways', {value:_asyncAlways, configurable:false})
Object.defineProperty(globalThis, 'reactiveFn', {value:_reactiveFn, configurable:false})
Object.defineProperty(globalThis, 'toggle', {value:_toggle, configurable:false})
Object.defineProperty(globalThis, 'map', {value:_map, configurable:false})
Object.defineProperty(globalThis, 'equals', {value:_equals, configurable:false})
Expand Down
54 changes: 54 additions & 0 deletions docs/manual/09 Functional Programming.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,60 @@ const output = await asyncAlways(() => (async (val) => val * 10)(input.val) ) //
> any dependency value after the first `await` is not captured.
> To avoid confusion, async transform functions are always disallowed for `asyncAlways`.
## Reactive functions

The `always` transform function is useful for inline transforms, but sometimes you might want to define
reusable reactive functions.

One way to achieve this is by wrapping the body of a normal function with `always`:

```ts
// normal function
const getGreetingMessage = (country: string) => {
switch (country) {
case "de": return "Hallo";
case "fr": return "Bonjour";
case "es": return "Hola";
default: return "Hello";
}
}

// reactive version
const getGreetingMessageReactive = (country: RefOrValue<string>) => {
// returns a Ref<string> that gets updated reactively
return always(() => {
switch (country) {
case "de": return "Hallo";
case "fr": return "Bonjour";
case "es": return "Hola";
default: return "Hello";
}
})
}
```

You can simplify this by just wrapping a normal function with `reactiveFn`.
Although the created reactive function is called with `Ref` values, you don't need to set the input argument types to `Ref` or `RefOrValue`:

```ts
// reactive function, returns a Ref<string> that gets updated reactively when 'country' changes
const getGreetingMessageReactive = reactiveFn((country: string) => {
switch (country) {
case "de": return "Hallo";
case "fr": return "Bonjour";
case "es": return "Hola";
default: return "Hello";
}
})

const country: Ref<string> = $$("de");
const greetingMessage: Ref<string> = getGreetingMessageReactive(country);

console.log(greetingMessage.val) // "Hallo"

country.val = "fr";
console.log(greetingMessage.val) // "Bonjour"
```

## Dedicated transform functions

Expand Down
28 changes: 27 additions & 1 deletion functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AsyncTransformFunction, CollapsedValue, CollapsedValueAdvanced, Decorat
import { Datex } from "./mod.ts";
import { PointerError } from "./types/errors.ts";
import { IterableHandler } from "./utils/iterable-handler.ts";

import { RestrictSameType } from "./runtime/pointers.ts";


/**
Expand Down Expand Up @@ -79,6 +79,32 @@ export async function asyncAlways<T>(transform:SmartTransformFunction<T>, option
return Ref.collapseValue(ptr) as MinimalJSRef<T>
}

/**
* Decorator for creating a reactive function.
* Functions decorated with `reactiveFn` always return a pointer that is automatically updated when input references are updated.
* This has the same effect as wrapping the function body with `always`.
* A reactive functions accepts references or values as arguments, but is always called with collapsed values.
* This means that you don't have to specifiy `Ref` values as arguments, but can use regular types.
*
* Example:
* ```ts
* // create reactive function 'getSquared'
* const getSquared = reactiveFn((x: number) => x * x);
*
* const x = $$(2);
* const y = getSquared(x); // Ref<4>
* x.val = 3;
* y // Ref<9>
* ```
*/
export function reactiveFn<ReturnType, Args extends unknown[]>(fn: (...args: Args) => Awaited<RestrictSameType<RefOrValue<ReturnType>>>) {
return (...args: MapToRefOrVal<Args>) => always(() => {
const collapsedArgs = args.map(arg => Ref.collapseValue(arg, true, true)) as Args;
return fn(...collapsedArgs)
});
}

type MapToRefOrVal<T extends unknown[]> = {[K in keyof T]: T[K] extends Ref ? T[K] : RefOrValue<T[K]>}


/**
Expand Down

0 comments on commit a80ec45

Please sign in to comment.