From e058e315ad1af9139f351d1e0d0f74fdbeb52e28 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 10 May 2022 15:52:13 +0000 Subject: [PATCH 1/5] Function charting --- .../src/components/FunctionChart.tsx | 121 ++++++++++++------ .../src/components/SquiggleChart.tsx | 65 +++++----- .../src/components/SquiggleEditor.tsx | 35 ++--- .../src/components/SquigglePlayground.tsx | 24 +++- packages/squiggle-lang/src/js/index.ts | 37 ++++-- .../squiggle-lang/src/js/rescript_interop.ts | 21 +++ .../src/rescript/TypescriptInterface.res | 3 + 7 files changed, 202 insertions(+), 104 deletions(-) diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index ea00aa9c..8e5abf32 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -1,18 +1,24 @@ import * as React from "react"; import _ from "lodash"; import type { Spec } from "vega"; -import type { Distribution, errorValue, result } from "@quri/squiggle-lang"; +import { + Distribution, + result, + lambdaValue, + environment, + runForeign, + errorValueToString, +} from "@quri/squiggle-lang"; import { createClassFromSpec } from "react-vega"; import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; import { DistributionChart } from "./DistributionChart"; +import { NumberShower } from "./NumberShower"; import { ErrorBox } from "./ErrorBox"; let SquigglePercentilesChart = createClassFromSpec({ spec: percentilesSpec as Spec, }); -type distPlusFn = (a: number) => result; - const _rangeByCount = (start: number, stop: number, count: number) => { const step = (stop - start) / (count - 1); const items = _.range(start, stop, step); @@ -27,51 +33,85 @@ function unwrap(x: result): a { throw Error("FAILURE TO UNWRAP"); } } +export type FunctionChartSettings = { + start: number; + stop: number; + count: number; +}; -function mapFilter(xs: a[], f: (x: a) => b | undefined): b[] { - let initial: b[] = []; - return xs.reduce((previous, current) => { - let value: b | undefined = f(current); - if (value !== undefined) { - return previous.concat([value]); - } else { - return previous; - } - }, initial); +interface FunctionChartProps { + fn: lambdaValue; + chartSettings: FunctionChartSettings; + environment: environment; } -export const FunctionChart: React.FC<{ - distPlusFn: distPlusFn; - diagramStart: number; - diagramStop: number; - diagramCount: number; -}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => { +export const FunctionChart: React.FC = ({ + fn, + chartSettings, + environment, +}: FunctionChartProps) => { let [mouseOverlay, setMouseOverlay] = React.useState(0); - function handleHover(...args) { - setMouseOverlay(args[1]); + function handleHover(_name: string, value: unknown) { + setMouseOverlay(value as number); } function handleOut() { setMouseOverlay(NaN); } const signalListeners = { mousemove: handleHover, mouseout: handleOut }; - let mouseItem = distPlusFn(mouseOverlay); + let mouseItem = runForeign(fn, [mouseOverlay], environment); let showChart = - mouseItem.tag === "Ok" ? ( + mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? ( ) : ( <> ); - let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount); - let valueData = mapFilter(data1, (x) => { - let result = distPlusFn(x); + let data1 = _rangeByCount( + chartSettings.start, + chartSettings.stop, + chartSettings.count + ); + type point = { x: number; value: result }; + let valueData: point[] = data1.map((x) => { + let result = runForeign(fn, [x], environment); if (result.tag === "Ok") { - return { x: x, value: result.value }; + if (result.value.tag == "distribution") { + return { x, value: { tag: "Ok", value: result.value.value } }; + } else { + return { + x, + value: { + tag: "Error", + value: + "Cannot currently render functions that don't return distributions", + }, + }; + } + } else { + return { + x, + value: { tag: "Error", value: errorValueToString(result.value) }, + }; } - }).map(({ x, value }) => { + }); + + let initialPartition: [ + { x: number; value: Distribution }[], + { x: number; value: string }[] + ] = [[], []]; + let [functionImage, errors] = valueData.reduce((acc, current) => { + if (current.value.tag === "Ok") { + acc[0].push({ x: current.x, value: current.value.value }); + } else { + acc[1].push({ x: current.x, value: current.value.value }); + } + return acc; + }, initialPartition); + + let percentiles = functionImage.map(({ x, value }) => { return { x: x, p1: unwrap(value.inv(0.01)), @@ -90,24 +130,25 @@ export const FunctionChart: React.FC<{ }; }); - let errorData = mapFilter(data1, (x) => { - let result = distPlusFn(x); - if (result.tag === "Error") { - return { x: x, error: result.value }; - } - }); - let error2 = _.groupBy(errorData, (x) => x.error); + let groupedErrors = _.groupBy(errors, (x) => x.value); return ( <> {showChart} - {_.keysIn(error2).map((k) => ( - - {`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`} + {_.entries(groupedErrors).map(([errorName, errorPoints]) => ( + + Values:{" "} + {errorPoints + .map((r) => ) + .reduce((a, b) => ( + <> + {a}, {b} + + ))} ))} diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 638dcb34..c833f9c3 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -6,7 +6,7 @@ import { errorValueToString, squiggleExpression, bindings, - samplingParams, + environment, jsImports, defaultImports, defaultBindings, @@ -14,6 +14,7 @@ import { import { NumberShower } from "./NumberShower"; import { DistributionChart } from "./DistributionChart"; import { ErrorBox } from "./ErrorBox"; +import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; const variableBox = { Component: styled.div` @@ -36,7 +37,7 @@ const variableBox = { interface VariableBoxProps { heading: string; children: React.ReactNode; - showTypes?: boolean; + showTypes: boolean; } export const VariableBox: React.FC = ({ @@ -66,9 +67,13 @@ export interface SquiggleItemProps { width?: number; height: number; /** Whether to show type information */ - showTypes?: boolean; + showTypes: boolean; /** Whether to show users graph controls (scale etc) */ - showControls?: boolean; + showControls: boolean; + /** Settings for displaying functions */ + chartSettings: FunctionChartSettings; + /** Environment for further function executions */ + environment: environment; } const SquiggleItem: React.FC = ({ @@ -77,6 +82,8 @@ const SquiggleItem: React.FC = ({ height, showTypes = false, showControls = false, + chartSettings, + environment, }: SquiggleItemProps) => { switch (expression.tag) { case "number": @@ -143,6 +150,8 @@ const SquiggleItem: React.FC = ({ height={50} showTypes={showTypes} showControls={showControls} + chartSettings={chartSettings} + environment={environment} /> ))} @@ -159,6 +168,8 @@ const SquiggleItem: React.FC = ({ height={50} showTypes={showTypes} showControls={showControls} + chartSettings={chartSettings} + environment={environment} /> ))} @@ -172,9 +183,11 @@ const SquiggleItem: React.FC = ({ ); case "lambda": return ( - - There is no viewer currently available for function types. - + ); } }; @@ -185,28 +198,22 @@ export interface SquiggleChartProps { /** If the output requires monte carlo sampling, the amount of samples */ sampleCount?: number; /** The amount of points returned to draw the distribution */ - outputXYPoints?: number; - kernelWidth?: number; - pointDistLength?: number; - /** If the result is a function, where the function starts */ - diagramStart?: number; - /** If the result is a function, where the function ends */ - diagramStop?: number; - /** If the result is a function, how many points along the function it samples */ - diagramCount?: number; + environment: environment; + /** If the result is a function, where the function starts, ends and the amount of stops */ + chartSettings?: FunctionChartSettings; /** When the environment changes */ onChange?(expr: squiggleExpression): void; /** CSS width of the element */ width?: number; height?: number; /** Bindings of previous variables declared */ - bindings?: bindings; + bindings: bindings; /** JS imported parameters */ - jsImports?: jsImports; + jsImports: jsImports; /** Whether to show type information about returns, default false */ - showTypes?: boolean; + showTypes: boolean; /** Whether to show graph controls (scale etc)*/ - showControls?: boolean; + showControls: boolean; } const ChartWrapper = styled.div` @@ -215,10 +222,10 @@ const ChartWrapper = styled.div` "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; `; +let defaultChartSettings = { start: 0, stop: 10, count: 100 }; export const SquiggleChart: React.FC = ({ squiggleString = "", - sampleCount = 1000, - outputXYPoints = 1000, + environment, onChange = () => {}, height = 60, bindings = defaultBindings, @@ -226,17 +233,9 @@ export const SquiggleChart: React.FC = ({ width, showTypes = false, showControls = false, + chartSettings = defaultChartSettings, }: SquiggleChartProps) => { - let samplingInputs: samplingParams = { - sampleCount: sampleCount, - xyPointLength: outputXYPoints, - }; - let expressionResult = run( - squiggleString, - bindings, - samplingInputs, - jsImports - ); + let expressionResult = run(squiggleString, bindings, environment, jsImports); let internal: JSX.Element; if (expressionResult.tag === "Ok") { let expression = expressionResult.value; @@ -248,6 +247,8 @@ export const SquiggleChart: React.FC = ({ height={height} showTypes={showTypes} showControls={showControls} + chartSettings={chartSettings} + environment={environment} /> ); } else { diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index 572dbd76..8e99182d 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -5,7 +5,7 @@ import { CodeEditor } from "./CodeEditor"; import styled from "styled-components"; import type { squiggleExpression, - samplingParams, + environment, bindings, jsImports, } from "@quri/squiggle-lang"; @@ -24,8 +24,6 @@ export interface SquiggleEditorProps { sampleCount?: number; /** The amount of points returned to draw the distribution */ outputXYPoints?: number; - kernelWidth?: number; - pointDistLength?: number; /** If the result is a function, where the function starts */ diagramStart?: number; /** If the result is a function, where the function ends */ @@ -55,13 +53,11 @@ const Input = styled.div` export let SquiggleEditor: React.FC = ({ initialSquiggleString = "", width, - sampleCount, - outputXYPoints, - kernelWidth, - pointDistLength, - diagramStart, - diagramStop, - diagramCount, + sampleCount = 1000, + outputXYPoints = 1000, + diagramStart = 0, + diagramStop = 10, + diagramCount = 100, onChange, bindings = defaultBindings, jsImports = defaultImports, @@ -69,6 +65,15 @@ export let SquiggleEditor: React.FC = ({ showControls = false, }: SquiggleEditorProps) => { let [expression, setExpression] = React.useState(initialSquiggleString); + let chartSettings = { + start: diagramStart, + stop: diagramStop, + count: diagramCount, + }; + let env: environment = { + sampleCount: sampleCount, + xyPointLength: outputXYPoints, + }; return (
@@ -82,14 +87,10 @@ export let SquiggleEditor: React.FC = ({ = ({ outputXYPoints = 1000, jsImports = defaultImports, }: SquigglePartialProps) => { - let samplingInputs: samplingParams = { + let samplingInputs: environment = { sampleCount: sampleCount, xyPointLength: outputXYPoints, }; diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 424cff8d..44fc4803 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -4,6 +4,11 @@ import ReactDOM from "react-dom"; import { SquiggleChart } from "./SquiggleChart"; import CodeEditor from "./CodeEditor"; import styled from "styled-components"; +import { + defaultBindings, + environment, + defaultImports, +} from "@quri/squiggle-lang"; interface FieldFloatProps { label: string; @@ -89,6 +94,15 @@ let SquigglePlayground: FC = ({ let [diagramStart, setDiagramStart] = useState(0); let [diagramStop, setDiagramStop] = useState(10); let [diagramCount, setDiagramCount] = useState(20); + let chartSettings = { + start: diagramStart, + stop: diagramStop, + count: diagramCount, + }; + let env: environment = { + sampleCount: sampleCount, + xyPointLength: outputXYPoints, + }; return ( @@ -105,15 +119,13 @@ let SquigglePlayground: FC = ({ diff --git a/packages/squiggle-lang/src/js/index.ts b/packages/squiggle-lang/src/js/index.ts index 961d4935..306e5a07 100644 --- a/packages/squiggle-lang/src/js/index.ts +++ b/packages/squiggle-lang/src/js/index.ts @@ -1,6 +1,5 @@ import * as _ from "lodash"; import { - samplingParams, environment, defaultEnvironment, evaluatePartialUsingExternalBindings, @@ -8,6 +7,7 @@ import { externalBindings, expressionValue, errorValue, + foreignFunctionInterface, } from "../rescript/TypescriptInterface.gen"; export { makeSampleSetDist, @@ -15,25 +15,30 @@ export { distributionErrorToString, distributionError, } from "../rescript/TypescriptInterface.gen"; -export type { - samplingParams, - errorValue, - externalBindings as bindings, - jsImports, -}; +export type { errorValue, externalBindings as bindings, jsImports }; import { jsValueToBinding, + jsValueToExpressionValue, jsValue, rescriptExport, squiggleExpression, convertRawToTypescript, + lambdaValue, } from "./rescript_interop"; import { result, resultMap, tag, tagged } from "./types"; import { Distribution, shape } from "./distribution"; -export { Distribution, squiggleExpression, result, resultMap, shape }; +export { + Distribution, + squiggleExpression, + result, + resultMap, + shape, + lambdaValue, + environment, +}; -export let defaultSamplingInputs: samplingParams = { +export let defaultSamplingInputs: environment = { sampleCount: 10000, xyPointLength: 10000, }; @@ -72,6 +77,20 @@ export function runPartial( ); } +export function runForeign( + fn: lambdaValue, + args: jsValue[], + environment?: environment +): result { + let e = environment ? environment : defaultEnvironment; + let res: result = foreignFunctionInterface( + fn, + args.map(jsValueToExpressionValue), + e + ); + return resultMap(res, (x) => createTsExport(x, e)); +} + function mergeImportsWithBindings( bindings: externalBindings, imports: jsImports diff --git a/packages/squiggle-lang/src/js/rescript_interop.ts b/packages/squiggle-lang/src/js/rescript_interop.ts index 45f4124b..0781f081 100644 --- a/packages/squiggle-lang/src/js/rescript_interop.ts +++ b/packages/squiggle-lang/src/js/rescript_interop.ts @@ -1,5 +1,6 @@ import * as _ from "lodash"; import { + expressionValue, mixedShape, sampleSetDist, genericDist, @@ -87,6 +88,8 @@ export type squiggleExpression = | tagged<"number", number> | tagged<"record", { [key: string]: squiggleExpression }>; +export { lambdaValue }; + export function convertRawToTypescript( result: rescriptExport, environment: environment @@ -168,3 +171,21 @@ export function jsValueToBinding(value: jsValue): rescriptExport { return { TAG: 7, _0: _.mapValues(value, jsValueToBinding) }; } } + +export function jsValueToExpressionValue(value: jsValue): expressionValue { + if (typeof value === "boolean") { + return { tag: "EvBool", value: value as boolean }; + } else if (typeof value === "string") { + return { tag: "EvString", value: value as string }; + } else if (typeof value === "number") { + return { tag: "EvNumber", value: value as number }; + } else if (Array.isArray(value)) { + return { tag: "EvArray", value: value.map(jsValueToExpressionValue) }; + } else { + // Record + return { + tag: "EvRecord", + value: _.mapValues(value, jsValueToExpressionValue), + }; + } +} diff --git a/packages/squiggle-lang/src/rescript/TypescriptInterface.res b/packages/squiggle-lang/src/rescript/TypescriptInterface.res index 6ebb8377..13763e72 100644 --- a/packages/squiggle-lang/src/rescript/TypescriptInterface.res +++ b/packages/squiggle-lang/src/rescript/TypescriptInterface.res @@ -84,3 +84,6 @@ type environment = ReducerInterface_ExpressionValue.environment @genType let defaultEnvironment = ReducerInterface_ExpressionValue.defaultEnvironment + +@genType +let foreignFunctionInterface = Reducer.foreignFunctionInterface From 70ea9c1b145a0e39b626b5a077be5a4e18649356 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 10 May 2022 16:08:12 +0000 Subject: [PATCH 2/5] Make optional arguments actually optional --- packages/components/src/components/SquiggleChart.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index e1407be5..13fd611a 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -213,15 +213,15 @@ export interface SquiggleChartProps { width?: number; height?: number; /** Bindings of previous variables declared */ - bindings: bindings; + bindings?: bindings; /** JS imported parameters */ jsImports?: jsImports; /** Whether to show a summary of the distirbution */ showSummary?: boolean; /** Whether to show type information about returns, default false */ - showTypes: boolean; + showTypes?: boolean; /** Whether to show graph controls (scale etc)*/ - showControls: boolean; + showControls?: boolean; } const ChartWrapper = styled.div` From 8d391f789d38b6a00fd51ca192800302b157ac54 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 10 May 2022 16:16:36 +0000 Subject: [PATCH 3/5] Keep props consistent --- .../src/components/SquiggleChart.tsx | 6 ++-- .../src/components/SquiggleEditor.tsx | 29 ++++--------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 13fd611a..241cd772 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -10,6 +10,7 @@ import { jsImports, defaultImports, defaultBindings, + defaultEnvironment, } from "@quri/squiggle-lang"; import { NumberShower } from "./NumberShower"; import { DistributionChart } from "./DistributionChart"; @@ -204,7 +205,7 @@ export interface SquiggleChartProps { /** If the output requires monte carlo sampling, the amount of samples */ sampleCount?: number; /** The amount of points returned to draw the distribution */ - environment: environment; + environment?: environment; /** If the result is a function, where the function starts, ends and the amount of stops */ chartSettings?: FunctionChartSettings; /** When the environment changes */ @@ -245,6 +246,7 @@ export const SquiggleChart: React.FC = ({ chartSettings = defaultChartSettings, }: SquiggleChartProps) => { let expressionResult = run(squiggleString, bindings, environment, jsImports); + let e = environment ? environment : defaultEnvironment; let internal: JSX.Element; if (expressionResult.tag === "Ok") { let expression = expressionResult.value; @@ -258,7 +260,7 @@ export const SquiggleChart: React.FC = ({ showTypes={showTypes} showControls={showControls} chartSettings={chartSettings} - environment={environment} + environment={e} /> ); } else { diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index 4b17b585..c4ac1876 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -21,9 +21,7 @@ export interface SquiggleEditorProps { /** The input string for squiggle */ initialSquiggleString?: string; /** If the output requires monte carlo sampling, the amount of samples */ - sampleCount?: number; - /** The amount of points returned to draw the distribution */ - outputXYPoints?: number; + environment?: environment; /** If the result is a function, where the function starts */ diagramStart?: number; /** If the result is a function, where the function ends */ @@ -55,8 +53,7 @@ const Input = styled.div` export let SquiggleEditor: React.FC = ({ initialSquiggleString = "", width, - sampleCount = 1000, - outputXYPoints = 1000, + environment, diagramStart = 0, diagramStop = 10, diagramCount = 100, @@ -73,10 +70,6 @@ export let SquiggleEditor: React.FC = ({ stop: diagramStop, count: diagramCount, }; - let env: environment = { - sampleCount: sampleCount, - xyPointLength: outputXYPoints, - }; return (
@@ -90,9 +83,8 @@ export let SquiggleEditor: React.FC = ({ = ({ initialSquiggleString = "", onChange, bindings = defaultBindings, - sampleCount = 1000, - outputXYPoints = 1000, + environment, jsImports = defaultImports, }: SquigglePartialProps) => { - let samplingInputs: environment = { - sampleCount: sampleCount, - xyPointLength: outputXYPoints, - }; let [expression, setExpression] = React.useState(initialSquiggleString); let [error, setError] = React.useState(null); @@ -181,7 +164,7 @@ export let SquigglePartial: React.FC = ({ let squiggleResult = runPartial( expression, bindings, - samplingInputs, + environment, jsImports ); if (squiggleResult.tag == "Ok") { From 930340e2f18c1f21c428f5da1628a531ee27f1ab Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 10 May 2022 16:20:31 +0000 Subject: [PATCH 4/5] Add default environment as export --- packages/squiggle-lang/src/js/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/squiggle-lang/src/js/index.ts b/packages/squiggle-lang/src/js/index.ts index 306e5a07..5549223b 100644 --- a/packages/squiggle-lang/src/js/index.ts +++ b/packages/squiggle-lang/src/js/index.ts @@ -36,6 +36,7 @@ export { shape, lambdaValue, environment, + defaultEnvironment }; export let defaultSamplingInputs: environment = { From ccb6938ad41efc7e4d8877567eb13bc6b38f181c Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 10 May 2022 16:24:08 +0000 Subject: [PATCH 5/5] Lint fix --- packages/squiggle-lang/src/js/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/squiggle-lang/src/js/index.ts b/packages/squiggle-lang/src/js/index.ts index 5549223b..d3d074fa 100644 --- a/packages/squiggle-lang/src/js/index.ts +++ b/packages/squiggle-lang/src/js/index.ts @@ -36,7 +36,7 @@ export { shape, lambdaValue, environment, - defaultEnvironment + defaultEnvironment, }; export let defaultSamplingInputs: environment = {