Spry: a programming language inspired by Smalltalk/Rebol and written in Nim
sprylang.org
sprylang.org
I like the cleanliness of Lisp, but I have always found it hard to read. That's one reason for Spry to have a bit more syntactic elements than Lisp.
Spry supports both prefix and keyword style function calls, although as a Smalltalker by heart I favour keyword style.
Finally, Spry is still an evolving experiment. Next up is the OO model which I hope will be interesting.
You mention that the OO model is not yet implemented, however, there are a few snippets of code on the homepage that look like OO. I can understand that when you implement `select:` as an infix function then `[1 2 3] select: [something]` is dispatched to that function instead of using polymorphic dispatch on the "receiver".
But, how are things like `clone` or `whileFalse:` implemented? Are they primitives? Are they using some other primitives under the hood?
I'm asking because while syntactically it looks like Smalltalk, i don't see how it could work the same way if there's no OO system under the hood. In Smalltalk, something like `whileFalse:` would be implemented as a method on the condition block, which, in turn, would execute itself and decide whether to recur or not by sending an `ifTrue:` (or `ifFalse:`) message on the resulting boolean. And the polymorphic implementation of that `ifTrue:` message on booleans would ultimately cause the loop to stop or not. In other words: there's no primitive "if", nor loops, on Smalltalk; it's message sending all the way down. Do you envision having some similar all-encompassing design principle on Spry too?
Also, kind of side note: i noticed that you're using the same syntax for blocks and lists. I understand this is something inherited from Rebol/Red, but i haven't used that language, so i don't really grok how that works. It's not something that most mainstream languages share, do you think you could dedicate some part of the language manual to explaining how can lists and blocks be "the same thing"? :D
Yes, the select: there is not polymorphic, it's simply a function (as of right now).
clone is a primitive function, can be seen here: https://github.com/gokr/spry/blob/master/src/spryvm.nim#L180... (search for "clone" on that page to find the Nim implementations for the various basic node types)
whileFalse: is also a primitive and can be seen here: https://github.com/gokr/spry/blob/master/src/spryvm.nim#L190...
So yes, these are examples of primitive Spry functions that are implemented in Nim.
Regarding the larger question, you can implement control structures in Spry, but they are obviously quite a lot slower. Also, it's worth noting that whileTrue: and friends are normally in Smalltalk implemented using special case bytecode tricks (jump).
But you are correct in wondering about polymorphism, and Rebol doesn't AFAIK have anything like it - Carl shunned OO when creating Rebol. In Spry I am working on a concept of tags and what I call polyfuncs. Any node can have one or more tags added to it and functions can have multiple sub functions dispatched over based on these tags. The basics of this already works:
https://github.com/gokr/spry/blob/master/src/sprytest.nim#L4...
Hard to read I guess, but it basically creates a polymorphic function "inc" that has two different sub funcs selected based on the tag of the receiver.
So about "design principle" - both yes and no. Spry is more "shallow" than typical Smalltalks because Spry is being implemented much more tightly together with Nim. This is by design, it makes it MUCH easier to make primitives and to interface with Nim and thus also C/C++. It's already trivial to do, and a lot of the examples show that. But I also want Spry to feel similarly "coherent" as Smalltalk does.
And yes, definitely will describe the block/list thing in the manual. Too bad this HN post happened before the manual is out there, but oh well.
But generally this is what homoiconicity means - code is in the same format as the core data structures. You can thus create and manipulate code as lists - just as in Lisp.
Rebol does have objects, that's what the O in Rebol refers to. Their prototypal objects influenced by the Self language.
And it also has polymorphism "built-in" [1] via its datatypes. However there doesn't seem to be a mechanism to extend or add your own. I guess the plan was probably to make use of the utype! datatype [2] penned for Rebol 3.
I think Carl Sassenrath is fine with OO but just doesn't like how most OO languages work [3].
[1] action! datatype - http://www.rebol.com/r3/docs/datatypes/action.html
[2] utype! - http://stackoverflow.com/questions/26780398/whats-known-abou...
[3] The problem with OOL is not the OO - http://www.rebol.com/article/0425.html
Further, I had read the article [3] and it seems to me that he indeed "rejected" OO in large parts after having worked with Smalltalk. That's quite intriguing (or astounding!) to me - since I agree that most so called OOP languages misses a lot of the story when compared to Smalltalk.
Personally I find polymorphism and encapsulation together with strong abilities of abstraction to be the key aspects of OO and I have never found any language that captures these things better than Smalltalk.
At the same time I also find Rebol's focus on DSLs to be very interesting - and as many may know this was also a big part of Alan Kay's FONC project.
In Spry I want to combine both. In contrast to Carl I embrace the original ideas of OO as manifested in Smalltalk. However, I couldn't care less of a lot of the overcomplicated mess that so called other OOP languages have created since then, and I also think the beauty of Smalltalk can be captured differently, for example without classes.
1(smallInt object) receive message 'to' with parameter 5 (smallInt object) return numeric range, then you send 'do; message with parameter being a block that evaluate for each element in range.
Smalltalk is about objects and message passing. Spry only use Smalltalk syntax.
(1 to: 5) do: [:x | Transcript show: x ]
there is actually a to:do: method in the Number class, for example see the GNU smalltalk documentation:https://www.gnu.org/software/smalltalk/manual-base/html_node...
And no, your description of how it works in Smalltalk is wrong - #to:do: is implemented in the receiver (1).
In Spry we are calling an infix function named to:do: and the only real difference to Smalltalk in this case is the fact that the dispatch of to:do: is not polymorphic. But as I described in other comments I am working on polymorphic functions as a way to reach similar abstraction levels.
Your infix functions would need to live somewhere probably in global scope with makes then no different than language constructs.
I would love to see new Smalltalk implementation that is not a Walled Garden. Making a Smalltalk impure is not a way foward imo. You work is interesting but what we need is Smalltalk VM on modern back-end like LLVM and better concurrency.
I know Smalltalk VERY well, and Spry is trying to experiment. I think polymorphic functions can be just as powerful as a class based model, even more so, and still feel similar. But i need to write examples to show it.
Regarding Nim - IMHO the main big pro is the fact that I can piggy back on the GC, use several of Nim's features (dynamic dispatch using methods for example), target all three of C/C++/js. And finally, I can use the very good C/C++ wrapping capabilities of Nim. As you may know wrapping C++ is ... very hard, but if you let Nim compile via C++ it works great.
I also find Nim very, very nice to work in.
And yeah, I hope Spry will show some interesting results of mixing Rebol/Red ideas with Smalltalk. And I do need to study Rebol/Red more. ;)
I also think there is quite a bit of difference in philosophy. Rebol/Red loves adding tons of hardwired datatypes into the language, while Spry is more Lisp/Smalltalkish in minimalism. Also, Rebol shunned OO which Spry doesn't (Red... not so sure).
But shunning OO is popular these days :)
Someone need to make lisp without the parentheses, where scoping can also be managed by indentation like in python. Throw in some strong imperative programming so that it's a lisp masquerading as C (because purism sucks), and you'd have one very powerful language
http://www.draketo.de/english/wisp/shakespeare
OpenDylan (I have heard it referred to HN as a Lisp without parens more than once):
And Rebol, not being lispy by the standards of few/some/many, is insane with macros from what I gather. Someone posted Red, a Rebol analogue that is an open source attempt to duplicate its expressiveness out of admiration.
http://www.red-lang.org/2015/12/answers-to-community-questio...
I am confused though because this is says macros are not there. I obviously don't use it, but the last HN thread about it made it sound crazy.
https://news.ycombinator.com/item?id=11364447
Anyway, re the strong imperative thing, someone posted Bone Lisp a couple days back, but this will not please you, it is classic parens Lisp, sans GC (I read but still have no clue how that works).
Dylan was supposed to be used for Apple's Newton handheld, but was too late and basically died in the 1990s. I'm no expert, but I recall it was basically a dumbed down Lisp with OO extensions and module support that was changed to a dumbed down Algol with OO extensions and module support.
It was basically an infix lisp with focus on hygiene (because it was basically a Lisp 1), and the possibility of sealing modules (making it impossible to alter / inherit from them ).
This IIRC; it's a long time ago...
So why is a standard trope about Lisp that with some acclimation the parentheses disappear? This is something that depends on what you're used to, and in some cases identifiable characteristics of individuals' visual processing, it's not a universal truth.
1. Multi-word lines without leading parens are grouped with later indented lines.
2. Indentation is not sensitive inside parens.
More info, unpacking the implications: https://github.com/akkartik/wart/blob/47e3572a29/004optional...
Infix works by using a disjoint set of 'operator characters' that isn't available to prefix symbols. More info: https://github.com/akkartik/wart/blob/47e3572a29/006infix
The parentheses of lisp provide a canonical serialization format. What you're actually asking for is "don't represent lists with brackets." Even Python uses brackets (and commas) for lists.
This actually describes the R programming language pretty well. A lisp with C-like syntax and vectorized primitives.
I.e. this
1 to: 5 do: [echo :x]
is pseudopython's loop(from=1, to=5, do=lambda x: echo x)
loop(1, 5, lambda x: echo x)
which have comparable punctuation, mostly trading "," for ":" .Being smalltalkish, you also get extra characters when a block is used for control structures, but that saves on the need to have two different syntaxes for single and multi-line/expression lambdas or N syntaxes for blocks and control structures.
How do you see that handled in your ideal language?
array at: 1 put: 2 ==> array at:put: 1 2
Thus a keyword call is purely syntactic sugar.
A Python indented Ruby would probably able to do without [ ] but I should think about whether there are some unresolvable ambiguities with that approach. I won't recommend that personally because syntactical spaces introduce bugs. I run into one a few days ago when I moved code around a file and forgot to fix the indentation of a few lines. Luckily this one cost me only five minutes. Many people like Python though.
Overall I like the terseness of
1 to: 5 do: [echo :x]
instead of the more verbose loop(1, 5, lambda x: echo x) # Python
(1..5).each do { |x| echo x } # Ruby
(1..5).each do |x| # Ruby again
echo x
end
But all those : are annoying: they increase the noise/signal ratio. I'd even do without the | | in the multi line Ruby version. I wonder if the compiler could add the : automatically by matching what it sees with the signatures of the defined function. 1 to 5 do [echo :x]
Then, why you need :x and couldn't use x without the : ?
Furthermore, how do I know how the name of the block argument if I didn't write the function. Isn't that an unnecessary coupling between the name chosen by the developer of the library and my code? I probably didn't understand everything is going on here. Edit: it has been explained in another comment in the parent's thread.And there are so many commonly used languages that use { } to define code blocks and [ ] for arrays. I'd stick with the majority for an easier onboarding of developers.
The possibility of defining pseudo keywords thanks to the infix/suffix arguments is great. That's probably anathema in the Python world (only one way of doing things) and also in Go's. It should be acceptable in Ruby's.
I'd prefer something like this
func (n)to(m)do(blk) {
x = n
...
do blk x
...
}
The order of arguments and function names is clear because you read them from left to right without going through two different lists.Is anybody using Spry for real world programs?
func (n)to(m)do(blk) {
x = n
...
do blk x
...
}
This looks ambiguous.
Your function does not have a name, how do you do if another library wants to do something different with the same keywords?
And how do you know when arguments should be evaluated? In your case, "blk" seems to be a set of statements to be executed when "do" is applied, is it right? shall we scan the body to detect if we use "do" on our arguments or does "do" in the signature have a special meaning?Let's see if we can make it work.
func (n)to(m)do(blk) could be syntactic sugar for to:do: funci (n, m, blk). I saw funci in the examples, I didn't investigate if there are other type of function definitions. However, if the definition starts with (arg) it should be easy to map it into a funci. This either solves the problem of name clashes with other libraries, or the problem is unsolved right now.
The block passing is more serious. Maybe we could mark blk in such a way we know it's a block. Ruby uses & as in
func (n)to(m)do(&blk)
Maybe Spry uses & in an incompatible way and I don't like it much anyway. It's developers bending to the compiler, but let's be realistic: we don't want slow compilers.What Ruby also does is having a yield keyword that calls an anonymous block passed at the invocation point of the function (a method in Ruby's world). The block is not declared as argument of the function/method. Maybe:
func (n)to(m)do() { # an empty arg is a block
x = n
...
do x
...
}
1 to 5 do { echo x }
But I think this is getting far away from the way of Spry.So it's a kind of quoting. I haven't pushed these things further yet, and frankly I am not that heavily into macros unless they are really needed. But obviously AST manipulation is easy in Spry.
{1 to 5 (echo x)}
... which would read into: (loop for x from 1 upto 5 do (echo x))
Note that binding `x` implicitly is bad style, as well as defining terse syntax for every construct. _to_do_
n to m do f =
x := n
...What you describe (interesting syntax) is a way to declare argument names and Spry doesn't declare them. That's a discussion in itself of course, but right now it doesn't.
There are no statement separators (!) in Spry and the grammar is very, very minimalistic. You could argue in some ways Spry is "a Lisp without parenthesis", and although I like indentation (Nim uses it) for scoping I also find Smalltalk VERY readable.
Also, it's not yet apparent but the syntax/grammar of Spry (and homoiconicity) makes it very suitable for DSL construction, very much similar to Rebol/Red.
Funny, that's an almost exact description of Nim :-)