daml2js: Memoize lazily constructed decoders (#6892)

Currently, if you have a record type `T` with a field of type, say,
`Optional S` and you're decoding a list of type `[T]`, then the decoder
for `S` has to be reconstructed for each element of the list. This is
because the `lazy` combinator from the `json-type-validation` does not
memoize the decoder it receives as a thunk.

This PR adds a new combinator `lazyMemo` which behaves like `lazy` but
also memoizes the decoder on its first invocation. All use sites of the
old `lazy` combinator are then replaced with `lazyMemo`.

We could consider upstreaming `lazyMemo` but I'm not sure how much
effort this is given that `json-type-validation` seems to be in
maintenance mode rather than active development.

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
Martin Huschenbett 2020-07-28 13:44:29 +02:00 committed by GitHub
parent a9fcf965ba
commit 11c4fe0727
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 42 additions and 5 deletions

View File

@ -505,7 +505,7 @@ renderDecoder = \case
"})"
DecoderConstant c -> "jtv.constant(" <> renderDecoderConstant c <> ")"
DecoderRef t -> snd (genType t) <> ".decoder()"
DecoderLazy d -> "jtv.lazy(function () { return " <> renderDecoder d <> "; })"
DecoderLazy d -> "damlTypes.lazyMemo(function () { return " <> renderDecoder d <> "; })"
data TypeDef
= UnionDef T.Text [T.Text] [(T.Text, TypeRef)]

View File

@ -1,6 +1,6 @@
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
import { Optional, Text } from './index';
import { Optional, Text, memo } from './index';
describe('@daml/types', () => {
it('optional', () => {
@ -22,3 +22,14 @@ describe('@daml/types', () => {
expect(dict.decoder().run([null]).ok).toBe(false);
});
});
test('memo', () => {
let x = 0;
const f = memo(() => {
x += 1;
return x;
});
expect(f()).toBe(1);
expect(f()).toBe(1);
expect(x).toBe(1);
});

View File

@ -90,6 +90,32 @@ export const lookupTemplate = (templateId: string): Template<object> => {
return template;
}
/**
* @internal Turn a thunk into a memoized version of itself. The memoized thunk
* invokes the original thunk only on its first invocation and caches the result
* for later uses. We use this to implement a version of `jtv.lazy` with
* memoization.
*/
export function memo<A>(thunk: () => A): () => A {
let memoized: () => A = () => {
const cache = thunk();
memoized = (): A => cache;
return cache;
};
// NOTE(MH): Since we change `memoized` when the resultung thunk is invoked
// for the first time, we need to return it "by reference". Thus, we return
// a closure which contains a reference to `memoized`.
return (): A => memoized();
}
/**
* @internal Variation of `jtv.lazy` which memoizes the computed decoder on its
* first invocation.
*/
export function lazyMemo<A>(mkDecoder: () => jtv.Decoder<A>): jtv.Decoder<A> {
return jtv.lazy(memo(mkDecoder));
}
/**
* The counterpart of DAML's `()` type.
*/
@ -211,7 +237,7 @@ export type List<T> = T[];
* Companion object of the [[List]] type.
*/
export const List = <T>(t: Serializable<T>): Serializable<T[]> => ({
decoder: (): jtv.Decoder<T[]> => jtv.lazy(() => jtv.array(t.decoder())),
decoder: (): jtv.Decoder<T[]> => lazyMemo(() => jtv.array(t.decoder())),
});
/**
@ -279,7 +305,7 @@ class OptionalWorker<T> implements Serializable<Optional<T>> {
constructor(private payload: Serializable<T>) { }
decoder(): jtv.Decoder<Optional<T>> {
return jtv.oneOf(jtv.constant(null), jtv.lazy(() => this.innerDecoder()));
return jtv.oneOf(jtv.constant(null), lazyMemo(() => this.innerDecoder()));
}
private innerDecoder(): jtv.Decoder<OptionalInner<T>> {
@ -323,7 +349,7 @@ export type TextMap<T> = { [key: string]: T };
* Companion object of the [[TextMap]] type.
*/
export const TextMap = <T>(t: Serializable<T>): Serializable<TextMap<T>> => ({
decoder: (): jtv.Decoder<TextMap<T>> => jtv.lazy(() => jtv.dict(t.decoder())),
decoder: (): jtv.Decoder<TextMap<T>> => lazyMemo(() => jtv.dict(t.decoder())),
});
// TODO(MH): `Map` type.