I'm getting closer to figuring this stuff out (I hope).
One problem with this super-simple system is that I wasn't able to figure out how functions fit into it... And fexprs just seemed to really muck things up even worse, and it's just a big pain and all.
But I wasn't really able to get rid of fexprs, or to be more specific, I couldn't find a better idea. Haskell has all this loveliness going on by using functions for pretty much everything, but I want to try a different route. I want to see how far I can push a functional OOP language with immutable objects and gensyms.
So for now, here's the plan. $vau will have to be built-in (duh) but using this system, it could in principle be written in user-land, provided you had a few other primitives like $quote, each, etc.
$vau will return an object. This object has a %call property, which lets you specify custom behavior when calling an object. This %call property is actually another object, which has an %argument, %environment, %body, and %closure properties.
Each property is a Nulan datatype, like an object or number or whatever, so this avoids the problem of using native JavaScript functions (I don't want to expose native things to Nulan that have the same purpose as Nulan data types, unless the reason is for speed).
That means that this vau:
($vau E [Args Body]
(wrap (eval E [$vau ~ Args Body])))
%closure would be the environment that the vau was defined in. This makes it possible not only to inspect the argument, body, environment, etc. of all vaus/functions, but also lets you modify them (keep in mind objects are immutable, so this doesn't cause any problems).
It also lets you attach arbitrary properties to vaus. Including other things, this means that (in principle) you can implement "wrap" in user-land:
$def %wrapped (uniq)
$def wrap
$vau E [F]
$let F (eval E F)
{ @($vau E X
(F (each X; X -> (eval E X))))
%wrapped F }
And then define $fn in terms of wrap, like usual:
$def $fn
$vau E [Args Body]
wrap (eval E [$vau ~ Args Body])
And then define "unwrap":
$def unwrap
$fn [{ %wrapped F }]
F
So I guess this means Nulan is going into the "everything is an object" direction, rather than the hardcore Haskell way which isn't too far from "everything is a function"
It'll be interesting to see how these immutable objects combine with the functional/recursive style of Nulan.
---
Oh yeah, and I've been playing with the idea of getting rid of var, so that there's no mutation whatsoever. Recursive functions would have to use the Y combinator. I'm not decided on this either.
of course here you can get away with just positional placement because all binary and keyword sends make it fairly clear where the message send is and where the argument is
: now terminates on (
; creates a new list in addition to terminating all previous : and ;
(before, it only terminated and didn't create a new list)
; now terminates ; in addition to :
I've actually changed my mind so that I don't mind using parens, but only for function calls. Calls to a vau (def, if, and, let, etc.) shouldn't be wrapped in parens:
mac accessor -> n v
uniqs %a
{def n -> %a
{{%a v} %a}}
def or: casevau env
x -> (eval x)
x @r -> let x (eval x)
if x x (eval {or @r})
The above is a hypothetical mockup for new syntax for Nulan. It's radically different than the current syntax, but I think it looks very clean. At least, compared to the current syntax:
$mac accessor; N V ->
$uniqs %A
[$def N; %A ->
[[%A V] %A]]
$defvau $or Env
X -> eval X
X @R -> $let X: eval X
$if X X: eval [$or @R]
Oh, I see what you're saying. You're saying ";" should have different meaning depending on whether it's to the left or the right of the symbol. That could work, but I feel it's making whitespace a bit too significant. No way to know for sure without trying it out, though.
well, you already make whitespace significant by saying that alphabeta is not alpha beta
the real crux of the issue is whether it comes before an identifier or after
don't think of it as whitespace disambiguating
nobody says *pointer in C is "too significant whitespace" just because it can't be separated by a space (in which case it becomes multiply!)
Like I said, I think it's only a bit too far, so if somebody wants to run with that idea, go for it. I personally favor the idea of having multiple syntaxes that parse to the same AST.
I didn't know you couldn't have a space after deref!
But, I dunno.. attaching ';' to identifiers is different from attaching '*'. Our brains are trained to treat the semi-colon as punctuation, never part of a word. Even programming languages have only reinforced this pattern. It's going to be a hard habit to break.
"Our brains are trained to treat the semi-colon as punctuation, never part of a word. Even programming languages have only reinforced this pattern. It's going to be a hard habit to break."
I would personally use something like | if it's not used for anything
it looks like an undirected paren so in cases like a |b| c it translates to a ((b) c) or (a (b)) c
I guess I'd pick the first option (more natural for reading left to right), but for the order of operations it doesn't matter
I agree that this is probably too much. Modern languages are training us to ignore the semi-colons; to have to pay attention to the whitespace around them seems retrogressive.
To be fair, the semicolons in Nulan are mostly used as infix operators, unlike in languages like C where they're required at the end of every statement. And Nulan already uses significant whitespace indentation. So I don't think it'd be a lot worse to make the whitespace significant for ":" or ";" I just think it might be a bit too far.
"Basically, is "traditional" (inasmuch as Arc establishes tradition) ssyntax prohibitively useful?"
I don't think so. Nulan completely ditched Arc's ssyntax and only uses significant whitespace, ":" and ";". Yet, despite that, it's capable of getting rid of almost all parentheses.
Oh, by the way, ":" in Nulan has a completely different meaning from ":" in Arc. I just chose it because I thought it looked nice.
It's not hard to write a macro in Arc/Nu that uses Racket's case-lambda, and in fact I had a semi-working version, but I gave that up when I started work on Nulan.
Unlike Arc, Nulan doesn't have optional args, so you have to use arity overloading:
$def foo
X Y -> X + Y # 2 argument version
X -> (foo X 10) # 1 argument version: default for Y is 10
Though I suppose it shouldn't be too hard to write a pattern matcher that supports optional args... something like this:
$def foo
X (opt Y 10) -> X + Y
In this case, "opt" is an object that has a %pattern-match type, which means users can write their own pattern matchers, unlike in Arc where it's all hardcoded.
"I'm not really concerned about bugs this can introduce. What I actually find odd is that it's taken me this long to notice. What else would I think "yeah, that should be an error" about, even though I've never actually run into the problem?"
Just an FYI: Arc/Nu does throw an error, because it uses Racket's optional-arg support. And even when it needs to fall back to full-blown destructuring, it still throws cryptic errors rather than doing nothing.
"The other option is to simply make strings, numbers, and symbols primitive. I'm actually really leaning toward this, but I'm not sure. The primary downside is that you can't just take a random string/number/symbol and slap arbitrary properties on it: they're atomic, just like types."
Okay, I think I got it figured out. Numbers, strings, and types will be atomic. Numbers and strings will just be JS numbers/strings, so they can be nice and faaaast. Symbols will be a tree that uses strings and has an %eval type, to give them special behavior when being evaluated.
There are a few more approaches if you're going for "everything's a foo" minimalism:
- Numbers and strings can be types. You already talk about using numbers as keys (aka types) for array access, so it's a bit surprising you haven't mentioned this option yourself.
- Numbers and strings can be objects whose types happen to be out of scope in user code. As far as the programmer is concerned, the core utilities were constructed with those types in scope, so it makes sense for them to be able to reach inside. I like the thought of everything being a weak table (and this philosophy underlies my garbage collector, Cairntaker), so this is the kind of choice I would make.
- Numbers and strings can be functions. Why not? :-p
Hm... I suppose that might work... yeah you're right that might be a really good idea. Thanks, I didn't think about that. But... hm... I wonder if that'll really work...
---
"Numbers and strings can be objects whose types happen to be out of scope in user code."
That's the problem. If I create the object using an ordinary closure to hide the data, how do I actually get the data when doing things like "lt" and "is" comparisons? I'd have to store it in the object itself, in which case now it's exposed to user code.
---
"Numbers and strings can be functions. Why not? :-p"
Because I'm trying to make a clean separation between data and algorithm. Though I already blur that a little by making $vau return an object. Plus I'm not sure how I'd implement that anyways, could you give an example?
"I'd have to store it in the object itself, in which case now it's exposed to user code."
If the type is out of scope in user code, then there's nothing user code can do to extract that value, right? The user code can't extract a list of types for an object, can it?
---
"Because I'm trying to make a clean separation between data and algorithm. Though I already blur that a little by making $vau return an object. Plus I'm not sure how I'd implement that anyways, could you give an example?"
I don't know what numbers and strings would do as functions, but they could potentially take a secret key and return some other internal representation. This secret key would not be in scope in user code. ;)
Of course, if user code can inspect lexical closure data anyway, it's impossible to hide things this way. I kinda recommend making the %closure key mean "the lexical closure, or nil if it happens to be unavailable."
"If the type is out of scope in user code, then there's nothing user code can do to extract that value, right? The user code can't extract a list of types for an object, can it?"
I was thinking about letting code loop through an object, similar to the "for ... in" loop in JavaScript. An example of where that would be helpful is map, filter, etc.
If I gave that up, then, sure, I could store it in the object and user code wouldn't be able to reach it.
---
"Of course, if user code can inspect lexical closure data anyway, it's impossible to hide things this way. I kinda recommend making the %closure key mean "the lexical closure, or nil if it happens to be unavailable.""
That's right. I think if I'm going to go the "everything is an object" way, then I want everything to be open. Of course, I might change my mind and try some other route...
I think I've figured out what it is that I want in a programming language, at least at a high level. I want a programming language that's basically like Lego blocks. You have these little atomic pieces that are useful in and of themself, but because they share a similar interface, you can plug them together to create bigger pieces.
In other words, it's the Unix philosophy of "write programs that do one thing and do it well" as well as the functional philosophy of "avoiding state". The reason for this is to be able to treat the program as a black box that can be plugged into other black boxes to get the behavior.
I've given up on extensibility and being able to hack other people's code. Why? If their code is insufficient, it's probably easier to just rewrite it from scratch anyways. As long as the new code has the same interface as the old code, it'll work just fine.
So I want a language that has a lot of power and flexibility for defining interfaces, and also a lot of fine-grained power for taking programs and treating them as black boxes. Using objects for everything defeats that, if I allow code to loop through the keys of objects.
But I think my objects + gensyms are basically a simple system that lets Nulan have both monads and comonads. Still a lot to think about and figure out!
Okay, I think I got it really figured out. Symbols and numbers will be atomic. Everything else is a tree: strings will be a list of symbols.
I'll have gensyms too, which are atomic like symbols, but are unique. It would be possible to build gensyms using just numbers + trees, but I think it's more consistent to make them atomic, since they're a kind of symbol.
Types then will be actual genuine gensyms, rather than almost-gensyms.