summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjsdelfino <jsdelfino@13f79535-47bb-0310-9956-ffa450edef68>2011-01-09 03:39:13 +0000
committerjsdelfino <jsdelfino@13f79535-47bb-0310-9956-ffa450edef68>2011-01-09 03:39:13 +0000
commitbf8b58267edaca28b2306da5300431e5a049896f (patch)
tree1b71c85f52715dc15369388174fd82f97a39b08f
parent2e9c610931b4e0a6bab25b1fe5dbd0def45ee126 (diff)
Add a composite similar to the travel tutorial app to the sample dashboard. Minor Javascript performance improvements to speed up the layout of big composites.
git-svn-id: http://svn.us.apache.org/repos/asf/tuscany@1056881 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--sca-cpp/trunk/modules/edit/domains/travel/app.composite240
-rw-r--r--sca-cpp/trunk/modules/edit/htdocs/edit/graph.js130
-rw-r--r--sca-cpp/trunk/modules/edit/htdocs/edit/index.html2
-rw-r--r--sca-cpp/trunk/modules/edit/workspaces/joe@localhost2
-rw-r--r--sca-cpp/trunk/modules/js/htdocs/elemutil.js14
-rw-r--r--sca-cpp/trunk/modules/js/htdocs/util.js27
6 files changed, 347 insertions, 68 deletions
diff --git a/sca-cpp/trunk/modules/edit/domains/travel/app.composite b/sca-cpp/trunk/modules/edit/domains/travel/app.composite
new file mode 100644
index 0000000000..833944a855
--- /dev/null
+++ b/sca-cpp/trunk/modules/edit/domains/travel/app.composite
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+-->
+<composite xmlns="http://docs.oasis-open.org/ns/opencsa/sca/200912"
+ xmlns:t="http://tuscany.apache.org/xmlns/sca/1.1"
+ targetNamespace="http://tuscanyscatours.com/"
+ name="travel">
+
+ <service name="SCAToursUserInterface" promote="SCAToursUserInterface"/>
+
+ <component name="SCAToursUserInterface" t:color="green">
+ <t:implementation.widget location="scatours.html"/>
+ <service name="Widget">
+ <t:binding.http uri="/scatours"/>
+ </service>
+ <reference name="scaToursCatalog" target="SCATours/SCAToursSearch" t:align="bottom">
+ <t:binding.jsonrpc/>
+ </reference>
+ <!-- Comment out multiple references wired to the same target -->
+ <!--
+ <reference name="scaToursBooking" target="SCATours/SCAToursBooking">
+ <t:binding.jsonrpc/>
+ </reference>
+ <reference name="scaToursCart" target="SCATours/SCAToursCart">
+ <t:binding.jsonrpc/>
+ </reference>
+ -->
+ </component>
+
+ <component name="SCATours">
+ <implementation.java class="com.tuscanyscatours.impl.SCAToursImpl"/>
+ <service name="SCAToursSearch" t:align="top">
+ <t:binding.jsonrpc/>
+ </service>
+ <!-- Comment out multiple services -->
+ <!--
+ <service name="SCAToursBooking">
+ <t:binding.jsonrpc/>
+ </service>
+ <service name="SCAToursCart">
+ <t:binding.jsonrpc/>
+ </service>
+ -->
+ <reference name="travelCatalogSearch" target="TravelCatalog/TravelCatalogSearch"/>
+ <reference name="tripBooking" target="TripBooking"/>
+ <!-- Comment out multiple references wired to the same target -->
+ <!--
+ <reference name="cartInitialize" target="ShoppingCart/CartInitialize"/>
+ -->
+ <reference name="cartCheckout" target="ShoppingCart/CartCheckout"/>
+ </component>
+
+ <component name="CreditCardPayment">
+ <implementation.java class="com.tuscanyscatours.payment.creditcard.impl.CreditCardPaymentImpl" />
+ <service name="CreditCardPayment">
+ <interface.wsdl interface="http://www.tuscanyscatours.com/CreditCardPayment/#wsdl.interface(CreditCardPayment)" />
+ <binding.ws uri="http://localhost:8082/CreditCardPayment" requires="authentication"/>
+ <binding.sca/>
+ </service>
+ </component>
+
+ <component name="CurrencyConverter">
+ <implementation.java class="com.tuscanyscatours.currencyconverter.impl.CurrencyConverterImpl" />
+ <service name="CurrencyConverter"/>
+ </component>
+
+ <component name="HotelPartner" t:color="orange">
+ <implementation.java class="com.tuscanyscatours.hotel.impl.HotelImpl"/>
+ <service name="Search">
+ <binding.ws name="searchws" uri="http://localhost:8086/Hotel/Search"/>
+ <!-- t:binding.jsonrpc name="searchjsonrpc"/-->
+ <callback>
+ <binding.ws/>
+ </callback>
+ </service>
+ <service name="Book"/>
+ </component>
+
+ <component name="FlightPartner" t:color="orange">
+ <implementation.java class="com.tuscanyscatours.flight.impl.FlightImpl"/>
+ <service name="Search">
+ <interface.java interface="com.tuscanyscatours.common.Search"
+ callbackInterface="com.tuscanyscatours.common.SearchCallback"/>
+ <binding.ws name="searchws" uri="http://localhost:8086/Flight/Search"/>
+ <t:binding.jsonrpc name="searchjsonrpc"/>
+ <callback>
+ <binding.ws/>
+ </callback>
+ </service>
+ <service name="Book"/>
+ </component>
+
+ <component name="CarPartner" t:color="orange">
+ <implementation.java class="com.tuscanyscatours.car.impl.CarImpl"/>
+ <service name="Search">
+ <binding.ws name="searchws" uri="http://localhost:8086/Car/Search"/>
+ <t:binding.jsonrpc name="searchjsonrpc"/>
+ <callback>
+ <binding.ws/>
+ </callback>
+ </service>
+ <service name="Book"/>
+ </component>
+
+ <component name="TravelCatalog">
+ <implementation.java class="com.tuscanyscatours.travelcatalog.impl.TravelCatalogImpl"/>
+ <service name="TravelCatalogSearch"/>
+ <!-- Wire to placeholder component representing external endpoint -->
+ <reference name="hotelSearch" target="HotelSearch">
+ <binding.ws uri="http://localhost:8086/Hotel/Search"/>
+ <callback>
+ <binding.ws name="callback"
+ uri="http://localhost:8084/Hotel/SearchCallback"/>
+ </callback>
+ </reference>
+ <!-- Wire to placeholder component representing external endpoint -->
+ <reference name="flightSearch" target="FlightSearch">
+ <interface.java interface="com.tuscanyscatours.common.Search"
+ callbackInterface="com.tuscanyscatours.common.SearchCallback"/>
+ <binding.ws uri="http://localhost:8086/Flight/Search"/>
+ <callback>
+ <binding.ws name="callback"
+ uri="http://localhost:8084/Flight/SearchCallback"/>
+ </callback>
+ </reference>
+ <!-- Wire to placeholder component representing external endpoint -->
+ <reference name="carSearch" target="CarSearch">
+ <binding.ws uri="http://localhost:8086/Car/Search"/>
+ <callback>
+ <binding.ws name="callback"
+ uri="http://localhost:8084/Car/SearchCallback"/>
+ </callback>
+ </reference>
+ <!-- Wire to placeholder component representing external endpoint -->
+ <reference name="tripSearch" target="TripSearch">
+ <binding.ws uri="http://localhost:8085/Trip/Search"/>
+ <callback>
+ <binding.ws name="callback"
+ uri="http://localhost:8084/Trip/SearchCallback"/>
+ </callback>
+ </reference>
+ <reference name="currencyConverter" target="CurrencyConverter">
+ </reference>
+ <property name="quoteCurrencyCode">GBP</property>
+ </component>
+
+ <component name="TripBooking">
+ <implementation.java class="com.tuscanyscatours.tripbooking.impl.TripBookingImpl"/>
+ <service name="TripBooking"/>
+ <reference name="hotelBook" target="HotelPartner/Book"/>
+ <reference name="flightBook" target="FlightPartner/Book"/>
+ <reference name="carBook" target="CarPartner/Book"/>
+ <reference name="tripBook" target="TripPartner/Book"/>
+ <reference name="cartUpdates" target="ShoppingCart/CartUpdates"/>
+ </component>
+
+ <component name="TripPartner" t:color="orange">
+ <implementation.java class="com.tuscanyscatours.trip.impl.TripImpl"/>
+ <service name="Search">
+ <binding.ws name="searchws" uri="http://localhost:8085/Trip/Search"/>
+ <t:binding.jsonrpc name="searchjsonrpc"/>
+ <callback>
+ <binding.ws/>
+ </callback>
+ </service>
+ <service name="Book"/>
+ </component>
+
+ <component name="ShoppingCart">
+ <implementation.java class="com.tuscanyscatours.shoppingcart.impl.ShoppingCartImpl"/>
+ <reference name="cartStore" target="CartStore" t:align="bottom"/>
+ <!-- Wire to placeholder component representing external endpoint -->
+ <reference name="payment" target="Payment">
+ <binding.ws uri="http://localhost:8081/Payment" />
+ </reference>
+ </component>
+
+ <component name="CartStore" t:color="yellow">
+ <!-- Add service element to specify alignment -->
+ <service name="CartStore" t:align="top"/>
+ <implementation.java class="com.tuscanyscatours.shoppingcart.impl.CartStoreImpl"/>
+ </component>
+
+ <component name="Payment">
+ <implementation.spring location="Payment-context.xml"/>
+ <service name="Payment">
+ <binding.ws uri="http://localhost:8081/Payment"/>
+ </service>
+ <!-- Wire to placeholder component representing external endpoint -->
+ <reference name="creditCardPaymentReference" target="CreditCardPayment">
+ <binding.ws uri="http://localhost:8082/CreditCardPayment" requires="authentication"/>
+ </reference>
+ <reference name="emailGateway" target="EmailGateway"/>
+ <reference name="customerRegistry" target="CustomerRegistry"/>
+ <property name="transactionFee">1.23</property>
+ </component>
+
+ <component name="CustomerRegistry">
+ <implementation.java class="com.tuscanyscatours.customer.impl.CustomerRegistryImpl" />
+ </component>
+
+ <component name="EmailGateway">
+ <implementation.java class="com.tuscanyscatours.emailgateway.impl.EmailGatewayImpl" />
+ </component>
+
+ <!-- Add placeholder components representing external endpoints -->
+ <component name="HotelSearch" t:color="red">
+ <service name="HotelSearch"/>
+ <implementation.java class="com.tuscanyscatours.PlaceHolderImpl" />
+ </component>
+ <component name="FlightSearch" t:color="red">
+ <service name="FlightSearch"/>
+ <implementation.java class="com.tuscanyscatours.PlaceHolderImpl" />
+ </component>
+ <component name="CarSearch" t:color="red">
+ <service name="CarSearch"/>
+ <implementation.java class="com.tuscanyscatours.PlaceHolderImpl" />
+ </component>
+ <component name="TripSearch" t:color="red">
+ <service name="TripSearch"/>
+ <implementation.java class="com.tuscanyscatours.PlaceHolderImpl" />
+ </component>
+
+</composite>
diff --git a/sca-cpp/trunk/modules/edit/htdocs/edit/graph.js b/sca-cpp/trunk/modules/edit/htdocs/edit/graph.js
index d2d5dab172..5f122ac498 100644
--- a/sca-cpp/trunk/modules/edit/htdocs/edit/graph.js
+++ b/sca-cpp/trunk/modules/edit/htdocs/edit/graph.js
@@ -120,9 +120,9 @@ if (graph.supportsVML()) {
document.body.appendChild(div);
var vmlg = document.createElement('v:group');
- vmlg.style.width = 500;
- vmlg.style.height = 500;
- vmlg.coordsize = '500,500';
+ vmlg.style.width = 2000;
+ vmlg.style.height = 2000;
+ vmlg.coordsize = '2000,2000';
div.appendChild(vmlg);
graph.dragging = null;
@@ -278,17 +278,17 @@ if (graph.supportsVML()) {
var d = graph.comppath(comp, cassoc).str();
var shape = document.createElement('v:shape');
- shape.style.width = 500;
- shape.style.height = 500;
- shape.coordsize = '500,500';
+ shape.style.width = 2000;
+ shape.style.height = 2000;
+ shape.coordsize = '2000,2000';
shape.path = d;
shape.fillcolor = graph.color(comp);
shape.stroked = 'false';
var contour = document.createElement('v:shape');
- contour.style.width = 500;
- contour.style.height = 500;
- contour.coordsize = '500,500';
+ contour.style.width = 2000;
+ contour.style.height = 2000;
+ contour.coordsize = '2000,2000';
contour.setAttribute('path', d);
contour.filled = 'false';
contour.strokecolor = graph.colors.gray;
@@ -301,9 +301,9 @@ if (graph.supportsVML()) {
var g = document.createElement('v:group');
g.id = scdl.name(comp);
- g.style.width = 500;
- g.style.height = 500;
- g.coordsize = '500,500';
+ g.style.width = 2000;
+ g.style.height = 2000;
+ g.coordsize = '2000,2000';
g.style.left = pos.xpos();
g.style.top = pos.ypos();
g.appendChild(shape);
@@ -514,41 +514,61 @@ if (graph.supportsSVG()) {
* Return the services and references of a component.
*/
graph.tsvcs = function(comp) {
- return filter(function(s) { return scdl.align(s) == 'top'; }, scdl.services(comp));
+ return memo(comp, 'tsvcs', function() {
+ var svcs = scdl.services(comp);
+ var l = filter(function(s) { return scdl.align(s) == 'top'; }, svcs);
+ if (isNil(l))
+ return mklist();
+ return mklist(car(l));
+ });
};
graph.lsvcs = function(comp) {
- var svcs = scdl.services(comp);
- if (isNil(svcs))
- return mklist("'element","'service","'attribute","'name",scdl.name(comp));
- var l = filter(function(s) { var a = scdl.align(s); return a == null || a == 'left'; }, scdl.services(comp));
- return l;
+ return memo(comp, 'lsvcs', function() {
+ var svcs = scdl.services(comp);
+ if (isNil(svcs))
+ return mklist(mklist("'element","'service","'attribute","'name",scdl.name(comp)));
+ var l = filter(function(s) { var a = scdl.align(s); return a == null || a == 'left'; }, svcs);
+ if (isNil(l))
+ return mklist();
+ if (!isNil(graph.tsvcs(comp)))
+ return mklist();
+ return mklist(car(l));
+ });
};
graph.brefs = function(comp) {
- return filter(function(r) { return scdl.align(r) == 'bottom'; }, scdl.references(comp));
+ return memo(comp, 'brefs', function() {
+ return filter(function(r) { return scdl.align(r) == 'bottom'; }, scdl.references(comp));
+ });
};
graph.rrefs = function(comp) {
- return filter(function(r) { var a = scdl.align(r); return a == null || a == 'right'; }, scdl.references(comp));
+ return memo(comp, 'rrefs', function() {
+ return filter(function(r) { var a = scdl.align(r); return a == null || a == 'right'; }, scdl.references(comp));
+ });
};
/**
* Return the color of a component.
*/
graph.color = function(comp) {
- var c = scdl.color(comp);
- return c == null? graph.colors.blue : graph.colors[c];
+ return memo(comp, 'color', function() {
+ var c = scdl.color(comp);
+ return c == null? graph.colors.blue : graph.colors[c];
+ });
};
/**
* Return the height of a reference on the right side of a component.
*/
graph.rrefheight = function(ref, cassoc) {
- var target = assoc(scdl.target(ref), cassoc);
- if (isNil(target))
- return 60;
- return graph.compclosureheight(cadr(target), cassoc);
+ return memo(ref, 'height', function() {
+ var target = assoc(scdl.target(ref), cassoc);
+ if (isNil(target))
+ return 60;
+ return graph.compclosureheight(cadr(target), cassoc);
+ });
};
/**
@@ -573,31 +593,37 @@ graph.brefsheight = function(refs, cassoc) {
* Return the height of a component.
*/
graph.compheight = function(comp, cassoc) {
- var lsvcs = graph.lsvcs(comp);
- var lsvcsh = Math.max(1, length(lsvcs)) * 60 + 20;
- var rrefs = graph.rrefs(comp);
- var rrefsh = graph.rrefsheight(rrefs, cassoc) + 20;
- var height = Math.max(lsvcsh, rrefsh);
- return height;
+ return memo(comp, 'height', function() {
+ var lsvcs = graph.lsvcs(comp);
+ var lsvcsh = Math.max(1, length(lsvcs)) * 60 + 20;
+ var rrefs = graph.rrefs(comp);
+ var rrefsh = graph.rrefsheight(rrefs, cassoc) + 20;
+ var height = Math.max(lsvcsh, rrefsh);
+ return height;
+ });
};
/**
* Return the height of a component and the components wired to its bottom side.
*/
graph.compclosureheight = function(comp, cassoc) {
- var brefs = graph.brefs(comp);
- var height = graph.compheight(comp, cassoc) + graph.brefsheight(brefs, cassoc);
- return height;
+ return memo(comp, 'closureheight', function() {
+ var brefs = graph.brefs(comp);
+ var height = graph.compheight(comp, cassoc) + graph.brefsheight(brefs, cassoc);
+ return height;
+ });
};
/**
* Return the width of a reference on the bottom side of a component.
*/
graph.brefwidth = function(ref, cassoc) {
- var target = assoc(scdl.target(ref), cassoc);
- if (isNil(target))
- return 60;
- return graph.compwidth(cadr(target), cassoc);
+ return memo(ref, 'width', function() {
+ var target = assoc(scdl.target(ref), cassoc);
+ if (isNil(target))
+ return 60;
+ return graph.compclosurewidth(cadr(target), cassoc);
+ });
};
/**
@@ -622,22 +648,26 @@ graph.rrefswidth = function(refs, cassoc) {
* Return the width of a component.
*/
graph.compwidth = function(comp, cassoc) {
- var twidth = graph.comptitlewidth(comp) + 20;
- var tsvcs = graph.tsvcs(comp);
- var tsvcsw = Math.max(1, length(tsvcs)) * 60 + 20;
- var brefs = graph.brefs(comp);
- var brefsw = graph.brefswidth(brefs, cassoc) + 20;
- var width = Math.max(twidth, Math.max(tsvcsw, brefsw));
- return width;
+ return memo(comp, 'width', function() {
+ var twidth = graph.comptitlewidth(comp) + 20;
+ var tsvcs = graph.tsvcs(comp);
+ var tsvcsw = Math.max(1, length(tsvcs)) * 60 + 20;
+ var brefs = graph.brefs(comp);
+ var brefsw = graph.brefswidth(brefs, cassoc) + 20;
+ var width = Math.max(twidth, Math.max(tsvcsw, brefsw));
+ return width;
+ });
};
/**
* Return the width of a component and all the components wired to its right side.
*/
graph.compclosurewidth = function(comp, cassoc) {
- var rrefs = graph.rrefs(comp);
- var width = graph.compwidth(comp, cassoc) + graph.rrefswidth(rrefs, cassoc);
- return height;
+ return memo(comp, 'closurewidth', function() {
+ var rrefs = graph.rrefs(comp);
+ var width = graph.compwidth(comp, cassoc) + graph.rrefswidth(rrefs, cassoc);
+ return width;
+ });
};
/**
@@ -759,7 +789,7 @@ graph.composite = function(compos) {
if (isNil(refs))
return mklist();
- return append(renderbref(car(refs), cassoc, pos), renderbrefs(cdr(refs), cassoc, pos));
+ return append(renderbref(car(refs), cassoc, pos), renderbrefs(cdr(refs), cassoc, rendermove(car(refs), cassoc, pos)));
}
var gcomp = graph.compshape(comp, cassoc, pos);
diff --git a/sca-cpp/trunk/modules/edit/htdocs/edit/index.html b/sca-cpp/trunk/modules/edit/htdocs/edit/index.html
index 2d88c1acad..34479a37a5 100644
--- a/sca-cpp/trunk/modules/edit/htdocs/edit/index.html
+++ b/sca-cpp/trunk/modules/edit/htdocs/edit/index.html
@@ -50,7 +50,7 @@ function editapp(name) {
return;
$('titleDiv').innerHTML = 'Editing: ' + name;
$('editDiv').innerHTML =
- '<iframe id="editFrame" style="visibility: visible; height: 100%; width: 100%; border: 0px;" scrolling="no" frameborder="0" src="edit.html?' +
+ '<iframe id="editFrame" style="visibility: visible; height: 5000px; width: 100%; border: 0px;" scrolling="no" frameborder="0" src="edit.html?' +
'app=' + name +
'"></iframe>';
}
diff --git a/sca-cpp/trunk/modules/edit/workspaces/joe@localhost b/sca-cpp/trunk/modules/edit/workspaces/joe@localhost
index 02c1af1263..b9419e75d0 100644
--- a/sca-cpp/trunk/modules/edit/workspaces/joe@localhost
+++ b/sca-cpp/trunk/modules/edit/workspaces/joe@localhost
@@ -1 +1 @@
-(("Sample Online Store App" "store" ()) ("Layout Variation of the Online Store App" "store2" ()) ("Another Variation of the Online Store App" "store3" ()) ("Sample HTTP Relay App" "relay" ()))
+(("Sample Online Store App" "store" ()) ("Layout Variation of the Online Store App" "store2" ()) ("Another Variation of the Online Store App" "store3" ()) ("Sample HTTP Relay App" "relay" ()) ("Travel Tutorial App" "travel" ()))
diff --git a/sca-cpp/trunk/modules/js/htdocs/elemutil.js b/sca-cpp/trunk/modules/js/htdocs/elemutil.js
index 00baab06c8..be976c3982 100644
--- a/sca-cpp/trunk/modules/js/htdocs/elemutil.js
+++ b/sca-cpp/trunk/modules/js/htdocs/elemutil.js
@@ -206,17 +206,21 @@ function selector(s) {
* Return the value of the attribute with the given name.
*/
function namedAttributeValue(name, l) {
- var f = filter(function(v) { return isAttribute(v) && attributeName(v) == name; }, l);
- if (isNil(f))
- return null;
- return caddr(car(f));
+ return memo(l, name, function() {
+ var f = filter(function(v) { return isAttribute(v) && attributeName(v) == name; }, l);
+ if (isNil(f))
+ return null;
+ return caddr(car(f));
+ });
}
/**
* Return child elements with the given name.
*/
function namedElementChildren(name, l) {
- return filter(function(v) { return isElement(v) && elementName(v) == name; }, l);
+ return memo(l, name, function() {
+ return filter(function(v) { return isElement(v) && elementName(v) == name; }, l);
+ });
}
/**
diff --git a/sca-cpp/trunk/modules/js/htdocs/util.js b/sca-cpp/trunk/modules/js/htdocs/util.js
index 0f966b180e..b90c96db2d 100644
--- a/sca-cpp/trunk/modules/js/htdocs/util.js
+++ b/sca-cpp/trunk/modules/js/htdocs/util.js
@@ -65,14 +65,8 @@ function reverse(l) {
}
function isNil(v) {
- if (v == null)
+ if (v == null || typeof v == 'undefined' || (v.constructor == Array && v.length == 0))
return true;
- if ('' + v == 'undefined')
- return true;
- try {
- if (isList(v) && v.length == 0)
- return true;
- } catch (e) {}
return false;
}
@@ -89,10 +83,8 @@ function isString(v) {
}
function isList(v) {
- try {
- if (v.constructor == Array)
- return true;
- } catch (e) {}
+ if (v != null && typeof v != 'undefined' && v.constructor == Array)
+ return true;
return false;
}
@@ -102,7 +94,11 @@ function isTaggedList(v, t) {
return false;
}
+var emptylist = new Array();
+
function mklist() {
+ if (arguments.length == 0)
+ return emptylist;
var a = new Array();
for (i = 0; i < arguments.length; i++)
a[i] = arguments[i];
@@ -206,3 +202,12 @@ function writeValue(v) {
return '(' + writeValue(car(v)) + writeList(cdr(v)) + ')';
}
+/**
+ * Apply a function and memoize its result.
+ */
+function memo(obj, key, f) {
+ if (obj[key])
+ return obj[key];
+ return obj[key] = f();
+}
+