"The thinking seems to go that the more you abstract away the hardware, the more understandable the language will be. But it seems to me that a concrete concept is easier to understand than an abstract one. The hardware gives you a concrete frame of reference."
The reason C "feels right" is because it matches the hardware, as you said.
But that doesn't mean high-level is bad: you can have high-level hardware. I'm sure on a Lisp machine, Lisp would feel right at home, whereas C wouldn't.
Unfortunately, I predict we'll be stuck with our von Neumann hardware for quite some time, which will severely hinder the progress of our software. No such thing as a free lunch, eh?
I didn't say high-level is bad. The biggest counter-example to that I can think of is pure mathematics. Clearly, mathematics is incredibly useful.
It just seems to me that, more often than not, the orthodoxy with respect to programming is wrong. Or, at best, partly wrong.
Another thing about programming languages designed for beginners is that they tend to be very strongly typed, e.g. Basic and Pascal. This is either because the designers thought it made things easier, or perhaps because they thought it instills some useful lesson. As far as making things easier, that doesn't seem to be true: Think of how many people out there that would flunk out of CS school are managing to write javascript and PHP.
Leaky abstractions is exactly right. At some point, all abstractions seem to break down. This is why I'm not sure the OO principle of encapsulation or information hiding is a good thing. The more you hide the implementation of something, the more you are forcing the use of an abstraction, and the harder it will be to go around the abstraction when, inevitably, you have to.
I don't think there is much you can say about programming languages without qualification. The range of programs you can write is too vast to generalize.
How do you feel about Lisp versus SML? I had to learn SML my second year in college, but at that time in my educational career I wasn't doing much studying, and I only half-learned it. I think some form of Lisp might have made a better introduction to functional programming, because you wouldn't have to deal with SML's type system at the same time.
I've never used SML and have only read a little about it. It looks a lot like Haskell, which I don't have much experience with either.
But from what I've seen, I don't like static type systems. I think making them optional is fantastic, but I don't like having them shoved down your throat.
I think you should be able to write your program in care-free Ruby/Arc style, and then once things have settled down, go back in and add type annotations for speed/safety. But you shouldn't have to use the type system right from the start.
The problem is that a lot of the people who find static type systems useful are also the kind of people who like safety a lot, so they want the type system on all the time. Not just to protect their code, but to prevent other programmers from making mistakes.
I don't like that mindset. Which is why I prefer languages like Ruby and Arc, even with their flaws. I don't think any restriction should be added in to prevent stupid people from doing stupid things. I think the language should only add in restrictions if it helps the smart people to do smart things. And for no other reason.
So as long as the type system helps smart people to do smart things, and doesn't get in the way too much, then sure, I think it's great. But if it gets in the way, or it's done to prevent stupid people from doing stupid things... no thanks.
In that line of reasoning, I've been thinking about adding in a static type checker to Nulan. But I want it to use a blacklist approach rather than a whitelist.
What I mean by that is, if it can be guaranteed at compile time that a program is in error, then it should throw a well formatted and precise error that makes it easy to fix the problem.
But if there's a chance that the program is correct, the type system should allow for it. This is the opposite of the stance in Haskell/SML which says: if it cannot be guaranteed at compile-time that a program is valid, then the program is rejected.
Here's an example of what I'm talking about:
def foo ->
bar 10 20
The variable `bar` isn't defined. This can be determined at compile-time. Thus, Nulan throws this error at compile-time:
NULAN.Error: undefined variable: bar
bar 10 20 (line 2, column 3)
^^^
The error message is precise, and pinpoints the exact source of the error, making it easy to fix. And likewise, this program...
def foo -> 1
5
foo 2
...creates a function `foo` that requires that its first argument is the number `1`. It then calls the function with the number `2`. This situation can be determined at compile-time, and so I would like for Nulan to throw this error:
...it might not be possible to determine whether the first argument to `foo` is the number `1` or not. If this were Haskell/SML, it might refuse to run the program. But in Nulan, I would simply defer the check to runtime.
This means that every program that is valid at runtime is also valid according to the type-checker. Thus the type-checker is seen as a useful tool to help catch some errors at compile-time, unlike Haskell/SML which attempt to catch all errors at compile-time.
I think this kind of hybrid system is better than pure dynamic/pure static typing.
How is this different from preventing stupid people from doing stupid things?
I've said this recently, but I like static typing when it contributes to the essential details of the program, rather than merely being a redundant layer for enhancing confidence in one's own code. Static typing is particularly meaningful at module boundaries, where it lets people establish confidence about each other's programs.
Anyway, enhanced usability is nothing to scoff at either. If you find this kind of static analysis important, I look forward to what you accomplish. :)
"How is this different from preventing stupid people from doing stupid things?"
Because the only difference is whether the error occurs at compile-time or run-time. I'm not adding in additional restrictions to make the type-system happy: if the type system can't understand it, it just defers the checking until run-time.
Thus, the type system takes certain errors that would have happened at run-time, and instead makes them happen at compile-time, which is better because it gives you early error detection. What the type system doesn't do is restrict the programmer in order to make it easier to detect errors at compile-time.
---
"If you find this kind of static analysis important"
Not really, no. Useful? Yeah, a bit. It's nice to have some early detection on errors. But my goals aren't to guarantee things. So whether you have the type-checker on or off just determines when you get the errors. A small bonus, but nothing huge. So I'd be fine with not having any static type checker at all.
The way I see it, what you're talking about still seems like a way to cater to stupid programming. Truly smart programmers don't generate any errors unless they mean to. ;)
---
"What the type system doesn't do is restrict the programmer in order to make it easier to detect errors at compile-time."
Guarantees don't have to "restrict the programmer." If you take your proposal, but add a type annotation form "(the <type> <term>)" that guarantees it'll reject any program for which the type can't be sufficiently proved at compile time, you've still done nothing but give the programmer more flexibility. (Gradual typing is a good approach to formalizing this sufficiency: http://ecee.colorado.edu/~siek/gradualtyping.html)
I think restriction comes into play when one programmer decides they'll be better off if they encourage other programmers to follow certain conventions, or if they follow certain conventions on their own without immediate selfish benefit. This happens all the time, and some may call it cargo culting, but I think ultimately it's just called society. :-p
"The way I see it, what you're talking about still seems like a way to cater to stupid programming. Truly smart programmers don't generate any errors unless they mean to. ;)"
Then I'll reclarify and say "any programmer who's just as smart as me", thereby nullifying the argument that a "sufficiently smart programmer would never make the mistake in the first place".
---
"If you take your proposal, but add a type annotation form [...]"
Sure, if it's optional. And not idiomatic to use it all the time. The problem that I see with languages that emphasize static typing is that even if it's technically possible to disable the type checker, it's seen as very bad form, and you'll get lots of bad looks from others.
The idioms and what is seen as "socially acceptable" matter just as much as whether it's "technically possible". If I add in type checking, it'll be in a care-free "sure use it if you want, but you don't have to" kind of way. I've seen very few languages that add in static typing with that kind of flavor to it.
---
"This happens all the time, and some may call it cargo culting, but I think ultimately it's just called society. :-p"
And I am very much so against our current society and its ways of doing things, but now we're straying into non-programming areas...
"And I am very much so against our current society and its ways of doing things, but now we're straying into non-programming areas..."
Yeah, I know, your and my brands of cynicism are very different. :) Unfortunately, I actually consider this one of the most interesting programming topics right now. On the 20th (two days ago) I started thinking of formulating a general-purpose language where the primitives are the claims and communication avenues people share with each other, and the UI tries its best to enable a human to access their space of feedback and freedom in an intuitive way.
I'd like to encourage others to think about how they'd design such a system, but I know this can be a very touchy subject. It's really more philosophy and sociology than programming, and I can claim no expertise. If anyone wants to discuss this, please contact me in private if there's a chance you'll incite hard feelings.
I think that C has a good solution. It will compile any code that's possible to compile, but it will output warnings. I don't think it's necessary to halt compilation just to get the programmer's attention. That's what Java does, and it really annoys me.
If the type-checking is not strictly necessary, maybe you should make it an option, like -Wall.
Yes, absolutely. There are certain errors that absolutely cannot be worked around, like an undefined variable. Those are errors that actually halt the program. But the rest should be optional.
I've learned through bitter experience to treat all C warnings as errors, and more. The presence of a single uninitialized local variable somewhere in your program makes the entire program undefined. Where undefined means "segfaults in an entirely random place."
I think that's a good practice in general. But when you are experimenting and debugging, it can be useful to eliminate chunks of code by expedient means, which often generates warnings that you don't care about.
I find programming to fractally involve debugging all the time. So if I allowed warnings when debugging I'd be dead :)
You're right that there are exceptions. I think of warnings as something to indulge in in the short term. The extreme short-term; I try very hard not to ever commit a patch that causes warnings. It really isn't that hard in the moment, and the cost rises steeply thereafter.
Incidentally, I'm only this draconian with C/C++. Given their utterly insane notions of undefined behavior I think it behooves us to stay where the light shines brightest. Whether we agree with individual warning types or not, it's easier to just say no.
But with other languages, converting errors to warnings is a good thing in general. Go, for example, goes overboard by not permitting one to define unused variables.
"The problem is that a lot of the people who find static type systems useful are also the kind of people who like safety a lot, so they want the type system on all the time. Not just to protect their code, but to prevent other programmers from making mistakes.
I don't like that mindset. Which is why I prefer languages like Ruby and Arc, even with their flaws. I don't think any restriction should be added in to prevent stupid people from doing stupid things. I think the language should only add in restrictions if it helps the smart people to do smart things. And for no other reason."
I could not agree more. I think that the idea of preventing mistakes via restrictive language features is one of the dominant ideas behind object-oriented languages. Consider the keywords "private" and "protected;" they literally have no effect other than to cause compile-time errors. It seems to me, intuitively, that the kinds of mistakes that can be easily caught by the compiler at compile time are in general the kinds of mistakes that are easily caught, period. The kinds of bugs that are hard to find are the ones that happen at runtime and propagate before showing themselves, and they are literally impossible for the compiler to find, because that would require the compiler to solve problems that are provably uncomputable. At my last job, I was working on fairly complicated web applications in PHP, and even though occasionally I'd run into a bug that could have been prevented by static type-checking, it was always in code that I had just written and wasn't hard to find. By eliminating things like variable declarations, PHP code can be made very succinct, and I think that simplicity and succinctness more than offset the risks that come from a permissive language. But I've never used a language that came with type-checking optional, so I have never made an apples-to-apples comparison. PHP is actually an interesting example, because in PHP, the rules for variable declarations are basically inverted from normal: you have to declare global variables in every function that you use them (or access them through the $GLOBALS array), but you don't have to declare function-scope variables at all. It makes a lot of sense if you think about it.
"Consider the keywords "private" and "protected;" they literally have no effect other than to cause compile-time errors."
Would you still consider this semantics restrictive if the default were private scope and a programmer could intentionally expose API functionality using "package," "protected," and "public"?
IMO, anonymous functions make OO-style private scope easy and implicit, without feeling like a limitation on the programmer.
---
"It seems to me, intuitively, that the kinds of mistakes that can be easily caught by the compiler at compile time are in general the kinds of mistakes that are easily caught, period."
I think that's true, yet not as trivial as you suggest. In general, the properties a compiler can verify are those that can be "easily" expressed in mathematics, where "easily" means the proof-theoretical algorithms of finding proofs, verifying proofs, etc. (whatever the compiler needs to do) have reasonable computational complexity. Mathematics as a whole is arbitrarily hard, but I believe human effort has computational complexity limits too, and I see no clear place to draw the line between what computers can verify and what humans can verify. Our type systems and other tech will keep getting better.
---
"The kinds of bugs that are hard to find are the ones that happen at runtime and propagate before showing themselves, and they are literally impossible for the compiler to find, because that would require the compiler to solve problems that are provably uncomputable."
I believe you're assuming a program must run Turing-complete computations at run time. While Turing-completeness is an extremely common feature of programming languages, not all languages encourage it, especially not if their type system is used for theorem proving. From a theorems-as-types point of view, the run time behavior of a mathematical proof is just the comfort in knowing its theorem is provable. :-p If you delay that comfort forever in a nonterminating computation, you're not proving anything.
Functional programming with guaranteed termination is known as total FP. "Epigram [a total FP language] has more static information than we know what to do with." http://strictlypositive.org/publications.html
---
"PHP is actually an interesting example, because in PHP, the rules for variable declarations are basically inverted from normal: you have to declare global variables in every function that you use them (or access them through the $GLOBALS array), but you don't have to declare function-scope variables at all. It makes a lot of sense if you think about it."
I find this annoying. My style of programming isn't absolutely pure functional programming, but it often approximates it. In pure FP, there's no need to have the assignment syntax automatically declare a local variable. That's because there's no assignment syntax! Accordingly, if a variable is used but not defined, it must be captured from a surrounding scope, so it's extraneous to have to declare it as a nonlocal variable.
I understand if PHP's interpreter doesn't have the ability to do static analysis to figure out the free variables of an anonymous function. That's why I would use Pharen, a language that compiles to PHP. (http://arclanguage.org/item?id=16586)
>> >> "Consider the keywords "private" and "protected;" they literally have no effect other than to cause compile-time errors."
>>
>> Would you still consider this semantics restrictive if the default were private scope and a programmer could intentionally expose API functionality using "package," "protected," and "public"?
Actually, in C++ the default for class members is private...
It's simply a true statement that "private" generates no machine language. All it does is cause compilation to fail. Whether or not this is a good thing is a matter of opinion.
>> IMO, anonymous functions make OO-style private scope easy and implicit, without feeling like a limitation on the programmer.
If you're speaking of lexical closures, I think you're right. You don't need to declare variables as private, because you can use the rules of scoping to make them impossible to refer to. You can achieve the same thing with a simpler syntax and more succinct code.
>> I believe you're assuming a program must run Turing-complete computations at run time. While Turing-completeness is an extremely common feature of programming languages, not all languages encourage it, especially not if their type system is used for theorem proving.
I'm not assuming that programming languages must be Turing complete. It happens to be true of all general-purpose languages that are in common use today.
>> Functional programming with guaranteed termination is known as total FP. "Epigram [a total FP language] has more static information than we know what to do with." http://strictlypositive.org/publications.html
I'll take a look at that language. I think that in 50 years' time, we might all be using non-Turing-complete languages. Making a language Turing complete is the easiest way to ensure that it can solve any problem, but isn't necessarily the best way.
( Technically, a language has to be Turing complete to solve literally any problem, but my hunch is that all problems of practical utility can be solved without resorting to a Turing machine.)
You should see a little green box in the upper-right panel. If you click on it, it should pop up a dialog box that says "Hi!"
---
At this point it's already a nicer experience than entering stuff into the REPL. But I can do better. I plan to add in CodeMirror[1] support for the textarea on the left side, and add some more polish and such.
Awesome, now there's line numbers! Later on I'll write up a mode that will add syntax highlighting and auto-indent to CodeMirror for Nulan, but for now this will do.
Firstly, there's now an interactive tutorial. Just open up "nulan.html", and if you don't see the tutorial, click the "Reset Tutorial" button at the top. Be careful, it'll clobber any changes you've made.
Secondly, you might have noticed that the editor has syntax highlighting now. But it's actually really cool. First, try inputting this:
$mac foo -> a b c
a + b + c
foo 1 2 3
def foo -> a b c
a + b + c
foo 1 2 3
Notice that it colored the first "foo" green because it's a macro, and the second "foo" orange because it's a function. It also colored local variables blue.
Now try inputting this:
# Array comprehensions
box in
$mac for -> x {('in) n y}
'y.map -> n x
$syntax-infix for 0 [ order "right" ]
$syntax-infix in 0 [ order "right" ]
(x + 2) for x in {1 2 3}
Notice that syntax is colored grey. Not just the built-in syntax, but user-created syntax is colored grey as well! It also understands delimiters:
$mac ^ -> a b
a + b
$syntax-infix ^ 0 [ delimiter %t ]
prn 1^2
Lastly, you can use "Debug mode" to toggle whether additional information is displayed in the lower-right panel or not.
You'll have to use the "Reset Tutorial" button to see all the changes.
The biggest change is that you can now click on an expression to highlight it. To see what I mean, try clicking on stuff in the tutorial.
But the coolest part is that the highlighting understands boxes. Try inputting this:
box foo = 5
foo + 10
box foo = 20
foo + 30
Now try clicking on the 1st or 2nd "foo". Then try clicking on the 3rd or 4th "foo". And it's so smart, it can even differentiate local variables (which is non-trivial because Nulan compiles top-level forms, like Arc):
Once again, try clicking the 1st or 2nd "foo", then the 3rd or 4th "foo".
It's also smart enough to know that the "." syntax is a string, even though it looks like a symbol:
box foo = []
foo.bar <= 5
foo.bar
So, obviously this isn't just a simple name search. The nice thing about this is, because Nulan uses boxes, it will only highlight the symbols that evaluate to the same box.
Imagine a search/replace function that operates on boxes rather than symbols: you'll be able to find/rename exactly the right symbols, rather than symbols that happen to have the same name.
"if-box is a kinda contrived example. Perhaps we can find something better?"
If you have a better suggestion, I'm all ears. But I settled on that because A) it's simple, B) it's short, C) it's something that does actually come up in practice, so it's not too contrived.
---
"What's the difference between w/uniq and w/box? Both seem to create boxes."
Well, to explain this... unique variables are actually implemented as anonymous boxes. So implementation-wise, there's not much difference.
The difference is in the actual macros "w/box" and "w/uniq". Here's the definition for them:
$mac w/box -> @args body
'w/new-scope
| var ,@args
| body
$mac w/uniq -> @args body
'w/box ,@(args.map -> x 'x = make-uniq;)
body
The answer is that "quote" inserts boxes for global symbols, but values for local symbols. That is, this macro...
$mac foo ->
w/box bar = 5
'bar + 2
...returns "5 + 2", not "#<box bar> + 2". This is an unfortunate inconsistency which happens because I'm using JavaScript variables for the sake of speed.
I thought about having it throw an error for local symbols, which would have required you to write the macro like this:
$mac foo ->
w/box bar = 5
',bar + 2
But I decided that it wasn't worth it. Now that I think about it, I wonder if it would be possible to hack something up so you no longer need w/uniq...
---
"I tried running this and got the following"
It works fine for me. And in fact, the code snippet you posted seems exactly the same as what is already in the tutorial. Maybe you intended to paste something else?
As for the error... yeah, there are still some cryptic errors. I plan to make them more meaningful later.
"Now that I think about it, I wonder if it would be possible to hack something up so you no longer need w/uniq..."
After thinking about it some more, I realized it won't work very well. It would require me to make all variables global, which would slow things down and be more verbose.
I pasted your DOM demo code twice, and then I was able to access both window.x and window.x2 from within Nulan. Is this intentional?
I also appended some DOM elements to the parent frame using "window.parent.document.append-child x", and they didn't get cleared on each update, but that's my own fault I guess. :-p
"I pasted your DOM demo code twice, and then I was able to access both window.x and window.x2 from within Nulan. Is this intentional?"
Yes, absolutely. It's how Nulan ensures that every variable is bound to a single "box". This also has the nice property that all the different "versions" of a variable can be accessed from the JavaScript side.
Ordinarily you can't access the different versions from within Nulan, but "window" is one of the ways you can do so. Another way would be the "&" macro which bypasses the Nulan compiler.
---
"I also appended some DOM elements to the parent frame using "window.parent.document.append-child x", and they didn't get cleared on each update, but that's my own fault I guess. :-p"
Yeah, only the <iframe> is sandboxed. Of course you can refresh the page to clear it out. Don't worry, it'll save the text you typed in the textarea.
It already has a command-line interface: just use "./nulan". I'm working on getting it to work with shell scripts too.
As for socket.io... well, that's just a JS library, right? And Nulan just compiles to raw JS, so you should be able to interface with it just fine.
As for online hosting... no, not really. I mean, if somebody wants to host it, feel free, but I'd personally wait until it's all nice and polished first.
"As for socket.io... well, that's just a JS library, right? And Nulan just compiles to raw JS, so you should be able to interface with it just fine."
I mean Node.js gives JavaScript the side effect powers typical of OS processes, while the browser gives JavaScript a UI. With socket.io, it should be very easy to take your browser interface and use it as a command shell. I can help if you'd like. ^_^
---
"As for online hosting... no, not really. I mean, if somebody wants to host it, feel free, but I'd personally wait until it's all nice and polished first."
I was thinking of something easy and free like GitHub Pages. I understand if you'd like some more polish though.
"I mean Node.js gives JavaScript the side effect powers typical of OS processes, while the browser gives JavaScript a UI. With socket.io, it should be very easy to take your browser interface and use it as a command shell. I can help if you'd like. ^_^"
Alrighty. I'm still not 100% sure how you'd accomplish that, so I wouldn't mind some help.
---
"I was thinking of something easy and free like GitHub Pages. I understand if you'd like some more polish though."
Well, before I consider any of that, I have a few cool ideas I want to add in first to "nulan.html".
After much effort (curse you JavaScript and your stupid statements!) I got all the Examples in the README working. Go on, try it out.
Now I'm working on making Nulan suitable for writing shell scripts. Then I'll rewrite my "playlist" program in Nulan. After that, I'll tackle writing a Chrome Extension in Nulan.
Second shameless plug: I've changed Nulan significantly since I last showed it off here. Here's a page describing Nulan's syntax system, which I believe is superior to all existing Lisp syntax systems:
If I follow everything on that page, this allows you to programmatically control where lists are segmented, and to generalize infix, resolving function/macro calls to be anywhere in the list. In other words, macro foo doesn't have to look like
(foo ...)
it can also look like
(... foo ...)
Does this make the grammar context-sensitive? Does it often introduce ambiguity? Have you run into non-terminating parsing? Can you talk about how it compares with oMeta? (Are you sure you aren't greenspunning it? :)
"If I follow everything on that page, this allows you to programmatically control where lists are segmented [...]"
Something like that, yes.
---
"[...] resolving function/macro calls to be anywhere in the list. In other words, macro foo doesn't have to look like"
Not quite. The parser is very simple: it just pushes symbols around. Which means that when you use a syntax rule like this:
$syntax-infix "foo"
Then the parser will rewrite this:
1 foo 2
Into this:
foo 1 2
This all happens before macros are expanded, so macros receive a uniform list representation. In this way, it's similar to wart's system, except that it's much more flexible and powerful.
---
"Does this make the grammar context-sensitive? Does it often introduce ambiguity? Have you run into non-terminating parsing?"
I have no idea, no, and no. It's all just simple list transformations, similar to what macros do.
---
"Can you talk about how it compares with oMeta? (Are you sure you aren't greenspunning it? :)"
I haven't use oMeta, but from what I understand, it uses something like PEG parsing, which is completely different.
Think of my system as being like macros: you take this list of symbols and that list of symbols and return this list of symbols.
The difference with macros is that my system has left/right associative, prefix/infix/suffix, precedence, and a slew of other options too.
The key insight is that unlike most syntax systems which return a single expression, Nulan's syntax system returns a list of expressions.
And then the syntax rules operate on this list, which effectively lets them look-behind and look-ahead arbitrarily many tokens, but only within the list.
PEG parsing lets you look-ahead as many tokens as you like, but not look-behind. Nulan's system supports both, but the amount of look-ahead/behind is controlled by the indentation, so everything is handled in a consistent way.
The - syntax rule has a precedence of 70, which is equal to 70, so it stops parsing. It now calls the action function for * with the arguments left, symbol, and right, which returns {* 50 20}
Now we go back to the + syntax rule, which looks like this:
It calls the action function for + with the arguments left, symbol, and right, which returns {foo {+ bar {* 50 20}}}:
left: {foo {+ bar {* 50 20}}}
remaining: {- 30}
Now it continues parsing with a precedence of 0. - has a precedence of 70 which is greater than 0, so it recursively calls the parser with a precedence of 70:
Only supports a single variable currently, though it would be cool to support multiple:
w/each x = {1 2 3}
prn x
Nulan's macro system is not only hygienic, but it plays very nicely with customizable syntax too, as you can see in the definition of "w/each":
$mac w/each -> {('=) x y} body
w/uniq i len
w/complex y
'w/var len = y.length
for (var i = 0) (i ~= len) (++ i)
w/var x = y[i]
body
And a couple more examples:
# Simulating a `for` loop using a `while` loop
$mac for -> init test incr body
'| init
| while test
| body
| incr
for (var i = 0) (i < 10) (++ i)
prn i
# Simulating a `do..while` loop using a `while` loop
$mac do -> body {('while) test}
'while %t
| body
| if ~ test
&break;
var i = 0
do
| prn i
| ++ i
while (i < 10)
P.S. Sorry for hijacking the thread, but I've been itching to talk about Nulan, since I've made so much progress on it (as shown by the many commits on Github).
Lisp is the obvious technically superior option for the web browser: rather than having three languages (HTML, CSS, and JS), you'd have one language: Lisp.
This would mean that things like SASS and HTML templates would be unnecessary: the HTML and CSS would be generated by Lisp macros, so you could use variables and other things.
Such a system would be incredibly elegant and would immediately solve lots of problems that HTML, CSS, and JS have. The reason why such a system hasn't been developed has little to do with technical reasons, but has to do with social reasons.
Most people don't make decisions based on whether it's a good idea or not, but based primarily upon what other people think. Most people play a power game involving reputation, honor, domination, putting other people down, raising their own ego, etc.
In such a world, these people see no gain in Lisp because it doesn't confer them any advantage in the power game they play, and HTML+CSS+JS already exists, so they prefer that.
In addition, these people believe that familiarity is the same thing as readability, thus because people are used to the status quo, they think that makes the status quo better. These people prefer to not take risks: they prefer inferior strategies that are normal compared to superior strategies that are unknown.
And from a more practical standpoint, if you're a boss of a company, it's much easier to find people who know HTML+CSS+JS than people who know Lisp.
Thus, the popular options remain popular, and the impopular options remain impopular, regardless of technical superiority or inferiority. This is true in almost all areas of our society, not just programming.
On a related note... even though we're stuck with the HTML+CSS+JS mess for the foreseeable future, there is the possibility of designing a Lisp that compiles down to JS. My own language Nulan is such a project. This would let you write Lispish code that executes very fast in the browser.
We also have ClojureScript, which is basically Clojure that compiles to Javascript: Lisp for the web browser. It combines the power of Clojure with Google's Closure web development library to spit out tight Javascript without any of the bad parts.
It's still not fully baked, but it's getting there. Clojure Lisp for web client development on top of Clojure on the desktop and server - my cup of Lisp doth brimmeth over.
I looked at ClojureScript at the source and perhaps I am wrong, but I like the simplicity of Arc and his approach.
But it is fine that others also go into this direction.
I think the difference is that to run an Arc powered website, you would need an Arc box on the server side, which is what you would need for running Clojure websites as well. What ClojureScript does is generate and compile the necessary JavaScript which can be off-loaded onto any (or at least most) web hosting sites, including shared web hosts. Additionally, the use of compiled JavaScript gives you the benefit of leveraging client-side processing, as opposed to Arc or Clojure boxes which are server-side solutions.
But you are right, Arc is great too, and it ultimately boils down to individual choice. Arc is my first Lisp, and I like it above Scheme/Common Lisp, as well as Python and Ruby.
But what if we not look at others and do our own. If somebody wants not to learn lisp and or see it advantages or decide to say in pride what make people blind.
My trainer said to me:
"What others do is NOT your problem. Do not judge."
So what if Lisp (Arc) never gets popular,
why is this a problem?
I am seeking for years for a better way of user interfaces
and one things for me is also the programming language as programming user of the computer.
akkartik: "I finally understood Pauan's diagnosis, that it was arc copying nil vs () terminated lists. I've been against that 'feature' for a long time."
Yes, but Arc/Nu manages to implement that Arc feature without conversion, so I don't consider it a problem with the feature, but a problem with the implementation.
---
akkartik: "+ and join on a single arg is just the identity function, right?"
Yes, but Arc does the conversion even if you pass in a single argument. So another possible solution to the problem is to change Arc to return the first argument unchanged when passed in a single argument.
---
zck: "If you call (+ '(((((((1))))))) '(((((((2)))))))) , you shouldn't have to change fifteen conses, just one or two."
Yes, you shouldn't have to. Arc uses ar-nil-terminate and ac-niltree to do the Arc->Racket->Arc conversion. ar-nil-terminate does a shallow copy, but ac-niltree does a deep copy. Simply changing ac-niltree wouldn't work, because I'm pretty sure other stuff depends on the deep copy.
However, the Arc compiler could be changed so that + uses a shallow version of ac-niltree. That's an interesting idea: how much stuff in the Arc compiler is currently using the deep ac-niltree when it could instead use a shallow ac-niltree?
"Arc uses ar-nil-terminate and ac-niltree to do the Arc->Racket->Arc conversion. ar-nil-terminate does a shallow copy, but ac-niltree does a deep copy.
However, the Arc compiler could be changed so that + uses a shallow version of ac-niltree."
That's the essence of the bug, as I see it. This fix is much shallower than the other fixes discussed, so this fix would make the most sense in a minimally updated variant of pg's Arc.
Should Anarki go for shallow or deep improvement? I've advocated shallow in the past, but now I'm thinking Arc oughta follow through on its promise to "break all your code," which of course means the entire network of hacks, libraries, help resources, and alternate language implementations we've built up so far. It would be nice to see Anarki become a fork of Arc/Nu and undergo deep improvements, without losing the Arc flavor.
"I feel uneasy about the shallow change to +; I'm sure the same bug exists in some other functions since deep niltree is the default."
From what I can see, there are only three places in the pg-Arc code where 'ac-niltree traverses too far, and they're all in ac.scm. Two are the definitions of + and ar-+2, and one is a misleading comment:
; Arc primitives written in Scheme should look like:
; (xdef foo (lambda (lst)
; (ac-niltree (scheme-foo (ar-nil-terminate lst)))))
; That is, Arc lists are NIL-terminated. When calling a Scheme
; function that treats an argument as a list, call ar-nil-terminate
; to change NIL to '(). When returning any data created by Scheme
; to Arc, call ac-niltree to turn all '() into NIL.
; (hash-table-get doesn't use its argument as a list, so it doesn't
; need ar-nil-terminate).
From another point of view, there are only a few places where 'ac-niltree probably needs to be recursive. Those are in the definitions of 'ac-call, 'ac-mac-call, and 'ac-macex, where they deal with quotation and macroexpansion, the two ways literal code is made available to Arc programs.
The other uses of 'ac-niltree are in 'ar-coerce (string to cons), 'dir, and 'timedate, where the lists are flat anyway.
I only looked for uses in pg-Arc, not any code that's been derived from it.
"Confusing that you're using deep/shallow with two meanings in the same comment :)"
Whoops! My comment was originally going to stand on its own, but I adapted it into a reply when Pauan got to what I wanted to say first. :) I didn't notice the word overlap.
Arc/Nu manages to implement that Arc feature without conversion
Hmm, can you elaborate on how it manages this? Is it just printing nil, but otherwise leaving the actual cons cells ()-terminated? (That option has probably been discussed multiple times: http://arclanguage.org/item?id=3094. I'm sure I mentioned it too at some point.)
"Hmm, can you elaborate on how it manages this? Is it just printing nil, but otherwise leaving the actual cons cells ()-terminated? (That option has probably been discussed multiple times: http://arclanguage.org/item?id=3094. I'm sure I mentioned it too at some point.)"
Yes. And then "nil" is a global variable that is bound to (). I also have to do a few other things like changing quote so that it changes the symbol "nil" to (), but overall it isn't that hard to have perfect Arc compatibility, even without doing the conversion.
16 lines total. Probably less than it would have taken to do the 'nil -> () conversion. But Arc can't tell the difference, unless I've overlooked something.
"I also just noticed: It is _utterly_ insane that niltree converts () to nil, while nil-terminate does the opposite."
It's called "tree" because it recurses on the car. In other words, it traverses sublists. As for why it's called "nil", well, I guess that's because you can't use () in the name without using || quotes. I'd call them "nil->null" and "null->nil". Or perhaps "arc->racket" and "racket->arc".
After about a week of work, I got a JS version of Nulan working well enough that I'm willing to show it off. For a taste of how it looks, check out NULAN.macros:
# function and infix syntax works
((-> a b c (a + b * c)) 1 2 3)
# some pattern matching works, some doesn't
((-> 5 10) 5)
((-> 5 10) 1)
((-> @a a) 1 2 3)
((-> @a b {a b}) 1 2 3)
# operator precedence works
(and (or 1 2) (or 3 4))
# almost anything can be used as an expression
-> (var foo 5)
# Arc-style "in"
(in 1 2 3 4 5)
And some more stuff too, like namespaces and the capability to eval things at compile time. I'll keep adding in things and fixing things as I come across them.
Yeah, definitely. The reason for the bug is because the Arc compiler uses Racket's "append" to do the join, which means it first needs to convert the Arc list to a Racket list, do the append, then convert back to an Arc list.
So there's two ways to fix this:
1) Change Arc to be like Arc/Nu, so that it doesn't need to do the conversion. I don't expect this to be too hard.
2) Write + in Arc, and have it call "join" when the first argument is a list. Then there's no discrepency between the two.
I actually dislike how the overloaded + is written in Arc, so I think having it call join instead would be great.
While I agree in general, I've found that S-expressions are easier to read than C syntax when you don't use syntax highlighting because the parens naturally group things together. With syntax highlighting, the situation changes so that syntax-rich languages can be significantly more readable.
I also find Ruby to be extremely readable, with or without syntax highlighting. Of course, I can only imagine how contorted and crazy Ruby's parser must be...
In any case, I tend to agree with Paul Graham on the principle that syntax should just be sugar for S-expressions. For instance, in Nulan:
-> a b c (a + b * c)
(&fn (&list a b c) (&add a (&mul b c)))
foo.bar
(&get foo "bar")
[ "foo" 1 "bar" 2 ]
(&dict "foo" 1 "bar" 2)
!(foo bar @qux)
(¬ (foo bar (&splice qux)))
All the syntax expands to plain old S-expressions which can be manipulated by macros.
It's pretty rare that I don't have syntax highlighting. Another thing about the parens that is annoying is that there doesn't seem to be a single, canonical way for them to interact with indentation, like K&R style for C.
I haven't used ruby, but I think terseness and readability tend to go hand in hand, and ruby looks terse.
Having a sugar -> S-expressions transform is great. It's the best of both worlds.
From what I understand, the "canonical way" is basically "whatever Emacs does", but I don't use Emacs. How I indent code (and how I have seen most other people indent code) is as follows:
2 spaces for indentation. Use indentation after any "block" expression:
(def foo (x)
(bar x))
(let foo 5
(bar foo))
Non-block expressions should be like so:
(foo 1 2 3 4)
(foo 1
2
3
4)
(foo 1
2
3
4)
That last one is generally rare and considered somewhat bad form, from what I understand.
Aside from "if", the Lisp code I've seen is generally very uniform, much more so than the JavaScript code I've seen, which can vary substantially from one person to another. For instance, when reading Racket code, it is usually very readable, and the only difference I've noticed is that in some situations they use 1 space rather than 2.
I think the reason Lisp doesn't have a "standard indentation style" is because it doesn't need one. There's no curly braces or distinction between statements and expressions, so people naturally tend to converge on a single style.
And even in the case of the different styles for "if", I at least choose whichever style works the best for me on a case-by-case basis, so that it looks the best. My general heuristic is as follows:
Use this style when there's only 3 arguments:
(if 1
2
3)
Use this style when there's more than 3 arguments:
(if 1
2
3
4)
Rarely use this style, only when all the arguments are short:
(if 1 2
3 4)
For Nulan, I haven't yet worked out the "if" idioms, but thus far I've used this form exclusively:
"The nice thing about indenting in C is simply that it falls out naturally, so you don't have to think about it."
In my experience with Java, Groovy, and JavaScript, the indentation gets just as idiosyncratic when it comes to nested function call expressions, long infix expressions, and method chaining.
I've rarely used C, but does its error code idiom obscure this drawback? I imagine error codes force programmers to put many intermediate results into separate variables, whether they like it that way or not.
In other words, you usually won't see this in C, right?
foo(bar(qux(1, 2, 3)))
Instead, you'd write it more like this:
x = qux(1, 2, 3)
if (x == ERR) {
...
}
x = bar(x)
if (x == ERR) {
...
}
x = foo(x)
if (x == ERR) {
...
}
Because you don't have nested expressions, there's only a single way to indent the code. But in languages like JavaScript, method chaining and nested function calls mean that there's now multiple ways to indent the same code.
Thus, C's syntax isn't actually more uniform than Lisp, it only seems that way because of C's way of handling errors.
Hmm, I'm losing track of my point. I prefer writing code so it models all errors explicitly, so I end up with that kind of verbosity in JavaScript too. I only get code like foo(bar(qux(x))) when I'm using lots of function calls that have no errors (or whose arguments can be errors).
>> C's syntax isn't actually more uniform than Lisp
C's syntax is actually less uniform, isn't it?
C isn't a very sophisticated language, but it tends to be readable. At least in the sense of following the flow of control; perhaps things like error handling make it hard to see the forest for the trees.
There may be a fundamental law that the more underpowered the language, the easier it is to read. Sort of like how Dr. Seuss books are more readable than research papers on programming languages theory, right?
I don't think Pauan was referring to C syntax as a whole. In this subthread, I think we've been specifically talking about whether certain languages have a "single, canonical" indentation style that "falls out naturally."
---
"There may be a fundamental law that the more underpowered the language, the easier it is to read. Sort of like how Dr. Seuss books are more readable than research papers on programming languages theory, right?"
In one sense that's true, since it's easy to make naive improvements to one feature while neglecting another. In another sense, a less readable language is always relatively "underpowered" due to its greater difficulty to use (assuming it's a language we use by reading :-p ).
I think C is a great language. It maps straightforwardly onto to the capabilities of the hardware. What I meant by calling it underpowered is that it doesn't do much to increase your power beyond freeing you from having to write assembly language.
Higher order functions and metaprogramming are the sort of things I associate with a powerful language, like Lisp. But sometimes things get so abstract you can't tell what you're looking at.
As you point out, it's easy to ruin something like a programming language while trying to improve it. (I haven't created a programming language, but I've used bad ones.)
> "There may be a fundamental law that the more underpowered the language, the easier it is to read."
That's a lot stronger claim than your original :) Aren't python, ruby, and haskell all high-power but easy to read?
There's the confounding effect of learnability; lisp gets more readable over time. There's also the confounding effect of density or difficulty. This quote captures both:
"If you're used to reading novels and newspaper articles, your first experience of reading a math paper can be dismaying. It could take half an hour to read a single page. And yet, I am pretty sure that the notation is not the problem, even though it may feel like it is. The math paper is hard to read because the ideas are hard. If you expressed the same ideas in prose (as mathematicians had to do before they evolved succinct notations), they wouldn't be any easier to read, because the paper would grow to the size of a book." (http://www.paulgraham.com/power.html)
I seem to have overlooked this post until just now...
Incidentally, I've never written python, ruby, or haskell, except for a tiny amount of python.
Good quote. I've been reading a lot of computer science papers lately, and I tend to skip over the math formulas and focus on the text. This could be because I'm reading them for "fun" and not because I have to for a class, or something. But I have always found it hard to take in dense notation, and preferred a conceptual argument. Maybe it's just that I have a deficiency in that area. But I think prose has the potential to carry powerful insights that are out of the reach of formulas; I suspect the problem is that succinct, brilliant prose is just incredibly hard to write. It's probably easier to just list formulas than to get deep ideas into prose. The reverse is also true, of course. Some ideas can only be expressed properly with notation.
But that probably has nothing to do with programming language syntax per se.
"I've been reading a lot of computer science papers lately, and I tend to skip over the math formulas and focus on the text."
I do that too. :) Unfortunately, at some point it gets hard to understand the prose without going back to read some of the fine details of the system they're talking about. XD
I tend to jump around. The introduction is usually boilerplate for the particular area of research, so it can be skipped. (I wonder how many different papers have told the story of the memory hierarchy and how it's getting more and more important as data gets bigger.) Then I try to figure out if the paper has anything important to say, before working on the math. I figure that sometimes the big idea of the paper is in the math, and other times, the big idea is in the text, and the math is just obligatory. (You can't publish a paper on an algorithm without spelling out the precise bounds on time and space, even if the formula contains 15 terms. Not all 15 terms can be important to the performance, but it certainly is important to put them in the paper.) I guess it depends on the field, but in the data structures papers I like to look at, it usually doesn't take a lot of math notation to express the key innovation.