Kevin Reid
2012-09-28 14:41:54 UTC
I was trying to get to sleep last night, and was struck by the thought of: If we were designing E from scratch (with the same goals), what would I actually do differently?
This is the list I repeatedly got out of bed to write, over a couple hours. It has at least two items which contradict each other. It probably has some items which are really bad ideas. It does not attempt any sort of consistent level of importance of its items. But I thought it might be mildly interesting.
Am I thinking of actually doing this, you ask? Well, at the moment it seems rather impractical with my available free time and to-dos. Inventing a new language is not inherently a good thing, *unless* it turned out that, for example, all current E code could be semi-automatically rewritten into the new language, thus reusing all that historical effort. On the other hand, I suspect that E *could* be more useful to the world right now if it weren't hampered by certain design choices.
(I'd be sticking this on the wiki if it weren't down at the moment...) (Sending to friam on the off chance it's an interesting topic.)
------------------------------------------------------------------------
Guidance
--------
• Early, build an implementation targeting JavaScript/SES, because what kind of modern language doesn't run "on the web"? However, allow only membraned interop with JavaScript code, because the alternative is to discard sameness.
• Aim for facilitating less-heavyweight implementations; fast interpreters, straightforwardly powerful compilers, small runtimes. (I have no idea how to put this in concrete terms to evaluate a design.)
• Start writing code with decent error handling from the start -- Make sure that every common error at all levels of abstraction produces nice results at the REPL as well as nice caller code. Use the feedback from this to get our error-propagation and error-value semantics right.
Call semantics (apply)
----------------------
• Coherent capability-styled error-propagation semantics. (The current problem is that post-sealed-throw, the natural synchronous error handling is ejectors and the natural asynchronous error handling is returning a broken reference, but there is no automatic conversion to be had. Perhaps sealed-throw was my mistake.)
• Errors are a sort of return value, broken references, but they're a sort of return value you can't incidentally-discard. Think of them like Maybe; the default action on a call is to match Just _.
If you want to pass on brokenness to your caller, you can explicitly choose to 'box a call' which is like a try{}; this gives you success-containing-failure (i.e. a slot containing a broken reference?)
TODO: Figure out whether this ruins the property that you can pass around a promise that may or may not have resolved to a broken reference.
(Perhaps the answer is that these failures are *not* broken references; they're one bit different from broken references. Boxing a call gets you a broken reference instead. You can also 'yield a call' which is explicitly passing on the broken reference you might receive. Hooray, we've invented too many kinds of calls!)
• Going the other direction, trying to have the ejector semantics in eventual programming: have a special argument-placeholder which means "The smash facet of the resolver for the innermost enclosing eventual send". (The eventual send syntactic sugar can set up a shadowing binding.)
• Consider defining errors to cause state rollback as well as nonlocal exit.
• Problem with E inheritance design: it's conflated with matchers, so type mirandas (respondsTo/getAllegedType) have the funny behavior of invoking the matcher.
• Think harder about how/if we want to provide property-access patterns; can we cooperate with methods-are-properties languages like JavaScript and Python? The original property access scheme failed because its fallback to get/set did not compose with inheritance, so it got watered down to just a getFoo/setFoo wrapper, but that is capitalization-based name mangling which is evil.
Kernel/AST/Eval
---------------
• Explicitly permit implementations to intern verbs.
• Prototype actual implementation of the ‘quasi-parser calls may be constant folded’ early.
• Keyword arguments of some sort; choose map syntax which is amenable to this or -- ooh ooh! -- build in _record objects_ to the language, which are optimized for a static set of keys.
Surface Syntax/Nonkernel AST
----------------------------
• Aim for a syntax with the elegance of Smalltalk. Try to reject keywords, or permit them only in ways which namespace them (e.g. my old idea of <keywords> like <this>).
• (Perhaps) Instead of environments which spill siblingward in the AST, provide such scoping as part of a general "omit the right bracket/flatten nesting" surface syntax. (I suspect that this doesn't actually work as it is less expressive, but it's worth testing, I think.)
• One way to test this: start with a Lisp-style surface syntax, and include a generic flattener.
• Can't be sufficient: consider objects defined within the arguments of a method call (e.g. a lexical version of the Measviz initialization).
• Build a simpler syntax, with less C-lineage-isms. Today's world is more diverse; the people who might be interested in E are less likely to be scared off by syntax. (Those who want familiar syntax can use JS/SES.) We tried tweaking our syntax towards C/Java to improve adoption and it didn't notably help.
• Syntax such that comments readily map to specific comment nodes in AST. This means that source code tools can refactor or prettyprint without losing comments.
• Make it easier to apply auditors all over the place; auditors on methods.
• Doc-comments-are-quasis from the ground up.
Naming
------
• Allow non-strings to be nouns and verbs, thus enabling
- trivial gensyms for expanded code. (Have a way to mark names as not-outside-this-scope so that we can choose to stringify them).
- avoidance of name conflicts in public "on this object" protocols.
• Idea: nominal-typed-interface objects which automatically provide name symbols.
interface foo { method x() } def object implements foo { method foo.x() {...} }
- Private protocols, as in the JS private name proposal — if we can make "not proxiable" work for us.
• Review Joule's design which contained non-string verbs.
• If nouns and verbs can be selfish, then also revisit the restriction of LiteralExpr to specific objects. Let's do better than Common Lisp!
• Start with some sort of module system that has no global namespace, or at least uses URIs as its global namespace.
• Avoid introducing any "well-known environment" issues in the distributed object protocol; start testing with application-local objects from the start.
Standard Library
----------------
• Build stream types into the standard library from the start, and design the library-idioms so that streams are cheap.
• Include a parser generator (PEG?) so as to quickly build self-hosting language tools and tools for data languages (e.g. XML, JSON) and little languages (e.g. regular expressions).
• Build HTML/XML/JSON quasis from the start, because interop with the web is a good thing.
• Design a coherent standard library, with less of a mishmash of Java-inherited objects plus E objects. Make sure that all the expected facilities of a modern language's library are reasonably available. (What are good resources for that?)
• Don't include synchronous file access.
• Do include POSIX interfaces so we aren't crippled by lack of e.g.
• file permission setting
• sockets
• capable terminal I/O
• Avoid the file-URL vs file-local-pathname confusion in the current API.
• (And on that note,) Figure out whether the URI-literal scheme can be kept without the every-URI-"scheme"-is-a-lexical-variable property.
This is the list I repeatedly got out of bed to write, over a couple hours. It has at least two items which contradict each other. It probably has some items which are really bad ideas. It does not attempt any sort of consistent level of importance of its items. But I thought it might be mildly interesting.
Am I thinking of actually doing this, you ask? Well, at the moment it seems rather impractical with my available free time and to-dos. Inventing a new language is not inherently a good thing, *unless* it turned out that, for example, all current E code could be semi-automatically rewritten into the new language, thus reusing all that historical effort. On the other hand, I suspect that E *could* be more useful to the world right now if it weren't hampered by certain design choices.
(I'd be sticking this on the wiki if it weren't down at the moment...) (Sending to friam on the off chance it's an interesting topic.)
------------------------------------------------------------------------
Guidance
--------
• Early, build an implementation targeting JavaScript/SES, because what kind of modern language doesn't run "on the web"? However, allow only membraned interop with JavaScript code, because the alternative is to discard sameness.
• Aim for facilitating less-heavyweight implementations; fast interpreters, straightforwardly powerful compilers, small runtimes. (I have no idea how to put this in concrete terms to evaluate a design.)
• Start writing code with decent error handling from the start -- Make sure that every common error at all levels of abstraction produces nice results at the REPL as well as nice caller code. Use the feedback from this to get our error-propagation and error-value semantics right.
Call semantics (apply)
----------------------
• Coherent capability-styled error-propagation semantics. (The current problem is that post-sealed-throw, the natural synchronous error handling is ejectors and the natural asynchronous error handling is returning a broken reference, but there is no automatic conversion to be had. Perhaps sealed-throw was my mistake.)
• Errors are a sort of return value, broken references, but they're a sort of return value you can't incidentally-discard. Think of them like Maybe; the default action on a call is to match Just _.
If you want to pass on brokenness to your caller, you can explicitly choose to 'box a call' which is like a try{}; this gives you success-containing-failure (i.e. a slot containing a broken reference?)
TODO: Figure out whether this ruins the property that you can pass around a promise that may or may not have resolved to a broken reference.
(Perhaps the answer is that these failures are *not* broken references; they're one bit different from broken references. Boxing a call gets you a broken reference instead. You can also 'yield a call' which is explicitly passing on the broken reference you might receive. Hooray, we've invented too many kinds of calls!)
• Going the other direction, trying to have the ejector semantics in eventual programming: have a special argument-placeholder which means "The smash facet of the resolver for the innermost enclosing eventual send". (The eventual send syntactic sugar can set up a shadowing binding.)
• Consider defining errors to cause state rollback as well as nonlocal exit.
• Problem with E inheritance design: it's conflated with matchers, so type mirandas (respondsTo/getAllegedType) have the funny behavior of invoking the matcher.
• Think harder about how/if we want to provide property-access patterns; can we cooperate with methods-are-properties languages like JavaScript and Python? The original property access scheme failed because its fallback to get/set did not compose with inheritance, so it got watered down to just a getFoo/setFoo wrapper, but that is capitalization-based name mangling which is evil.
Kernel/AST/Eval
---------------
• Explicitly permit implementations to intern verbs.
• Prototype actual implementation of the ‘quasi-parser calls may be constant folded’ early.
• Keyword arguments of some sort; choose map syntax which is amenable to this or -- ooh ooh! -- build in _record objects_ to the language, which are optimized for a static set of keys.
Surface Syntax/Nonkernel AST
----------------------------
• Aim for a syntax with the elegance of Smalltalk. Try to reject keywords, or permit them only in ways which namespace them (e.g. my old idea of <keywords> like <this>).
• (Perhaps) Instead of environments which spill siblingward in the AST, provide such scoping as part of a general "omit the right bracket/flatten nesting" surface syntax. (I suspect that this doesn't actually work as it is less expressive, but it's worth testing, I think.)
• One way to test this: start with a Lisp-style surface syntax, and include a generic flattener.
• Can't be sufficient: consider objects defined within the arguments of a method call (e.g. a lexical version of the Measviz initialization).
• Build a simpler syntax, with less C-lineage-isms. Today's world is more diverse; the people who might be interested in E are less likely to be scared off by syntax. (Those who want familiar syntax can use JS/SES.) We tried tweaking our syntax towards C/Java to improve adoption and it didn't notably help.
• Syntax such that comments readily map to specific comment nodes in AST. This means that source code tools can refactor or prettyprint without losing comments.
• Make it easier to apply auditors all over the place; auditors on methods.
• Doc-comments-are-quasis from the ground up.
Naming
------
• Allow non-strings to be nouns and verbs, thus enabling
- trivial gensyms for expanded code. (Have a way to mark names as not-outside-this-scope so that we can choose to stringify them).
- avoidance of name conflicts in public "on this object" protocols.
• Idea: nominal-typed-interface objects which automatically provide name symbols.
interface foo { method x() } def object implements foo { method foo.x() {...} }
- Private protocols, as in the JS private name proposal — if we can make "not proxiable" work for us.
• Review Joule's design which contained non-string verbs.
• If nouns and verbs can be selfish, then also revisit the restriction of LiteralExpr to specific objects. Let's do better than Common Lisp!
• Start with some sort of module system that has no global namespace, or at least uses URIs as its global namespace.
• Avoid introducing any "well-known environment" issues in the distributed object protocol; start testing with application-local objects from the start.
Standard Library
----------------
• Build stream types into the standard library from the start, and design the library-idioms so that streams are cheap.
• Include a parser generator (PEG?) so as to quickly build self-hosting language tools and tools for data languages (e.g. XML, JSON) and little languages (e.g. regular expressions).
• Build HTML/XML/JSON quasis from the start, because interop with the web is a good thing.
• Design a coherent standard library, with less of a mishmash of Java-inherited objects plus E objects. Make sure that all the expected facilities of a modern language's library are reasonably available. (What are good resources for that?)
• Don't include synchronous file access.
• Do include POSIX interfaces so we aren't crippled by lack of e.g.
• file permission setting
• sockets
• capable terminal I/O
• Avoid the file-URL vs file-local-pathname confusion in the current API.
• (And on that note,) Figure out whether the URI-literal scheme can be kept without the every-URI-"scheme"-is-a-lexical-variable property.
--
Kevin Reid <http://switchb.org/kpreid/>
Kevin Reid <http://switchb.org/kpreid/>