If you like everything being an expression, check out tcl. A lot of ideas from lisp show up in tcl, especially the idea of everything as an expression. Tcl embodies this idea while also having the look of an algol-like language. Funny it can pull this off while having basically no syntax.
The empty tuple/unit is not the same as void. It has exactly one possible value, void has zero possible values. A way to write it in Rust is `enum Void {}` (an enumeration with no options).
The point is that both void and Unit can only produce a side effect. In everything-is-an-expression based languages Unit is exactly equivalent to void in C*.
Indeed, and there has been some talk of removing this restriction in C++ because it makes certain kinds of metaprogramming a lot more cumbersome than they should be.
True, neither are useful as values, though Unit typically conveys programmer intent (to produce a side effect), whereas the empty tuple is, in Scala at any rate, quite rare.
The empty tuple:
scala> val empty = Tuple1(())
empty: (Unit,) = ((),)
The difference between an empty tuple (also known as unit) and void becomes obvious when you deal with vaguely complex trait impls. For example, if you have a trait:
How would you specify that your type implements Foo in such a way that bar() cannot return an error? If you were to implement it using the empty tuple (unit), like this, it could actually return an error:
struct Abc{}
impl Foo for Abc {
type ErrorType = ();
fn bar() -> Result<u8, ()> {
Err(()) // oops, we don't want to be able to do that!
}
}
Instead, you can use Void here:
enum Void{}
struct Xyz{}
impl Foo for Xyz {
type ErrorType = Void;
fn bar() -> Result<u8, Void> {
// No way to create a Void, so the only thing we can return is an Ok
Ok(1)
}
}
Aside from the obvious power to express intent (can we return an error without any information attached, or can we not error at all?), this would allow an optimizing compiler to assume that the result of Xyz::bar() is always a u8, allowing it to strip off the overhead of the Result:
fn baz<F: Foo>(f: F) {
match f.bar() {
Ok(v) => println!("{}", v),
Err(e) => panic!("{}", e)
};
}
...
baz(Xyz); // The compiler can notice that f.bar() can never return a Result::Err, so strip off the match and assume it's a Result::Ok
A super-smart compiler would even make sure it's not storing the data for "is this an Ok or Err" in the Result<u8, Void> at all.
Finally, similarly, you can specify that certain functions are uncallable by having them take Void as a parameter.
Regardless of who is right, you are arguing the wrong bit of it. I contend that, to a person, everyone saying "C void is Unit" doesn't think C void is Void. Arguing that Void is not Unit is just completely spurious. Of course Void is not Unit. No one disagrees.
In truth, C void is not exactly either Void or Unit. Like Void, you can't exactly make one... but you can call functions declared to take it and write functions that return it, and really it just means "I have no information to pass" - which is more like Unit.
You can't create a Void, and you can't cast to it either - it's not a bottom type. So you can't put anything in bar. And as a result, you can't create a reference to a Void, so you can't call foo. And abc and xyz just can't be implemented in the first place.
On the other hand, you can do all of these just fine:
fn foo(v: ()){ ... }
fn bar(v: &()){ ... }
...
let v = ();
bar(&v);
foo(v);
The fact that you can create and use an empty tuple as a value shows that it is not equivalent to Void.
(All statements here are made within the safe subset of the language - unsafe allows access to intrinsics that would allow a Void to be made, and a reference to Void.)
This example of "everything as an expression" doesn't take it as far as Tcl, though. In the above code snippet, the conditional body is surrounded by braces, which are syntax. In Tcl, the second argument to the 'if' command is also an expression, which only uses braces as a quoting mechanism, if it needs to.
But the "then" and "else" parts of the Rust "if" are expressions. What you're talking about doesn't seem to be related to which things are expressions, it's more like being able to eval code at runtime.
The difference is that C/Java/Algol have different syntaxes for things-as-expressions and things-as-statements, and you can't put blocks in the things-as-expressions. In Rust, blocks are also expressions and so have a result (the result of the last expression in the block), so your expressions inside the if can be as complex as you like.
Since functions also have a block, and the return value of the function is the result of the block, this is much more consistent.
In GNU C, you can use a brace-enclosed statement block as an expression.
Funny story; years before I became a C programmer, and at a time when I didn't yet study ISO C properly, I discovered and used this extension naturally.
I wanted to evaluate some statements where only an expression could be used so I thought, gee, come on, can't you just put parens around it to turn it into an expression and get the value of the last expression as a return value? I tried that and it worked. And of course, if it works it's good (standards? what are those?)
Then I tried using the code with a different C compiler; oops!
Anyway, this GNU C feature doesn't have as much of an impact as you might think.
I see two issues with the ternary operator. One, the syntax is much less readable, and two, the consequent and alternate are both single expressions, so you can't do something like:
x = if(something) {
a = foo();
baz(a);
} else {
b = bar();
baz(b);
}
x = (something) ? ({ a = foo(); baz(a) }) : ({ b = bar(); baz(b); });
However, since all your forms are actually expression statements, we can happily just use the ISO C comma operator:
x = (something) ? (a = foo(), baz(a)) : (b = bar(); baz(b));
If C provided operators for iteration, selection and for binding some variables over a scope (that scope consisting of an expression), everything would be cool. E.g. fantasy while loop:
x = (< y 0) ?? y++ : y; // evaluate y++ while (< y 0), then yield y.
Variable binding:
x = let (int x = 3, double y = 3.0) : (x++, x*y);
The problem is that some things can only be done with statements.
The ternary operator is not actually lacking anything; with the comma operator, multiple expressions can be evaluated. What's lacking is the vocabulary of what those expressions can do.
a and b have to be defined before the statement containing the terniary expression here. And the point is that you can embed arbitrary multi-statement logic in your if expressions in Rust in the same way you'd do it anywhere else.
Tcl does have a lispy feel to it, but s-expressions in Lisp are much more elegant than strings in Tcl, imho. Greenspun called Tcl the Lisp without a brain[1], which can be taken both as a compliment or an insult.
Tcl commands are lists, not strings. Or more precisely, they are coercible to strings or lists, but a well-formed Tcl command string is always coercible to a well-formed Tcl list (not all Tcl strings are coercible to lists).
It used to be strings all the way down before the object system was implemented in 8.0.
That incidentally is part of the reason why the expr command was created to process infix expressions since it would be too costly to keep converting sub-expressions back and forth from strings to numbers.
Interesting. There was this company called Vignette (in the same period). They too were doing .com projects for clients, and, IIRC, using AOLServer which I read was in Tcl. Tcl was used for the projects.
Although they've been working on TCL perf (and even compilation) and there have been some improvements, especially when you're not doing metaprogramming. But still, using it on heavy loads isn't a great idea...
(I mean, honestly. I'm starting to think picolisp might be faster, and picolisp has 3 types and one data structure. Haven't run the benches yet, though.)
Yeah the level of dynamicness (dynamism?) you can get in tcl is unparalleled as far as I can see. Having no types or syntax and access to the entire runtime at any point in the program opens up all kinds of crazy doors. But you're right, that slows it down.
But you might be interested to know there is currently an effort to get Tcl to compile to native/near-native code. Here is a paper on the new techniques being developed and a link to a talk given by one of the lead tcl core team members.
Most languages of this sort, like Smalltalk, Lisp, and especially slower, more liberal implementations like PicoLisp, allow for compile-time and/or runtime AST transformation, and other sorts of metaprogramming.
in TCL, everything is a string. Or at least, everything behaves like a string in the proper context. When you pass code blocks into a command (like if, or while, or whatever), you're not passing code objects: you're passing unevaled strings. This is why expr works in TCL: there's nothing special about expr, it's just actually implementing a DSL (sort of) rather than evaling your code straight up.
The practical upshot of this is that unlike smalltalk (I think: Can an ST user actually answer this?), and to a greater degree than LISP and FORTH (;immediate and readtables are a lot more painful to wrangle), you can not only modify the semantics of the language: you can modify the syntax.
Smalltalk just like Lisp has very little syntax, almost everything is built on messages, including data type creation, conditionals and loops, among other things.
Also you can at any time just completely replace one object by other via the becomes: message.
There are also some cool tricks when metaclasses are used, many of each one can see in Python as well.
However, IIRC, unlike Lisp, ST doesn't have any AST transformation or parsing hooks (macros and readtables, in Lisp parlance), so while ST has a lot of the semantic extension capabilities of Lisp (indeed, it's more semantically extensible than some of the less Object Oriented Lisps), it lacks the syntactic capabilities for extension.
As far as I can remember no (Smalltalk was long time ago for me, 1995).
But I think there is already quite a few things possible via messages and metaclasses, even if one cannot do actual AST transformations.
After all, the whole image is accessible, so you can dynamically ask any object for its definition, or even compiled code (bytecode or JIT) and change them.
>But I think there is already quite a few things possible via messages and metaclasses, even if one cannot do actual AST transformations.
That's true. In fact, there are things that messages and metaclasses can do that you can't do with AST transformations without implementing those abstractions.
>After all, the whole image is accessible, so you can dynamically ask any object for its definition, or even compiled code (bytecode or JIT) and change them.
I still miss this in Lisp. Some Lisps had that, once, but it's uncommon nowadays. it happens in the commercial CLs, but those are expensive. OS CL implementations rarely have good image support (in SBCL, image saving actually corrupts the RAM state to the point of nonrecoverability, and the docs recomend fork(2)ing if you want to save an image and continue your app).
Aside from the proprietary CLs, PicoLisp is the only modern lisp environment that has this kind of dynamic capability AFAICT (and while it does sort of have images, in the form of external symbols, the language doesn't encourage using them like this. Also, calling it modern is a stretch: it has more in common with LISP 1.5 than, say, CL). And the Schemes? Don't make me laugh. Scheme has many strengths, but reflection isn't one of them. It's something that I really wish the Lisps had.
One of the reasons I want to try my hand at implementing Lisp on the Spur VM at some point.
I just read a bit of documentation from the Picolisp page. It looks really cool, but can it reach arbitrarily far up the call stack? That's the quality of tcl that I don't see other places. The capabilities of 'upvar' and 'uplevel'.
[Edit] I should say "one of the qualities." The other important one is that Tcl has no types. Even all the lisps I know have types.
The functions you're looking for are called `up` and `run` in picolisp.
And before you ask, yes, picolisp has list interpolation (or quasiquoting, in lisp parlance), so you can control which parts of a certain chunk of code will be run when, and in which contexts. It also includes the `macro` fexpr, which makes interpolation more convenient.
Yes, everything is a string. But you can pass data around anywhere you like without worrying about coercing. A command will receive its data in string representation and treat it however it likes. So when you go to do math with the 'expr' command, 'expr' will treat its arguments as numbers.
set x 5
expr { $x + 3 }
'expr' receives x as the string 5 but knows to treat it as an int.
I'm not sure that's a great explanation. Maybe a decent summary of the concept of 'no types' is "the language does not presume to tell you how you can or cannot use your data."
Thanks. Yes, I get it. I had read part of the Ousterhout book. And also Don Libes' Expect book uses Tcl, since Expect is written in it. Had read a lot of that too. Both very good ones. Didn't get to use Tcl in projects though. I'd read some years ago that Tcl was used heavily in the electronics / EDA industry.
Pity that it became so associated with the Tk GUI toolkit -- half the Linux GUI apps in the 1990s were in Tcl/Tk, and when Tk fell out of favor so did Tcl.
...And with TTK, you can finally have easy-to-write cross-platform UIs that actually look native.
Seriously. If you want a decent cross-platform UI system with minimal effort, which has bindings in just about every language, TK is really worth your time now.
Tk UIs will never be acceptably native as long as scrollbars remain a separate control from the thing being scrolled. This stops OSes deciding where the scrollbars should go based on input device and locale.
On OSX in particular, Tk UIs always stick out like a sore thumb for this reason, even with TTK.
Should have been made available on the web by some browser vendor in the 90s. Netscape invented a language, Sun wanted to embed Java, but went with the applet approach in lieu of Netscape, Microsoft put vbscript in IE.
Everything is an expression in Ruby, yes. Even `class` and `def` and `module`, for example. Though class, as a specific example, returns nil, so it's not terribly useful that it is one.
In Lisp, "def..." macros, like defclass, defun, etc. return the symbol to which is bound the definition. Not essential, but useful at times. I tend to find Lisp is full of details like this.
In the Scheme language, many imperative forms have an unspecified result. For instance, see R7RS 4.1.6: "the result of the set! expression is unspecified".
Also, a related misfeature is that function arguments can be evaluated in any order.
Yes, this is one of the things that I really want to punch the SSC for. Assignment should have a useful return value: even C, so often said to do The Wrong Thing, got that right.
But for those of you who aren't scheme programmers, it gets worse: because when RnRS says that the result of something is "unspecified" many implementations take it literally. That's right: in many Schemes, `set!` returns the literal value #<unspecified>. I swear I'm not making this up. It's awful.
To quote Jonathan Gabriel of Penny Arcade: "Baby, why you always gotta make me hit you?"
Yes, sorry, Common was implied (not claiming it should always be, but here I wrote it without thinking about others).
Scheme's unspecified results are annoying indeed.
I don't know many other tcl hackers, and I'm really interested in what the community looks like right now. I put that there to drum up conversation with a tcl hacker.