4

TL;DR is val name <- more readable than name val <-?

I'm designing a semi-concatenative, postfix language. I haven't given much thought to the style in which variable are assigned, and now I see that there are two (main) ways to do it. Originally, a variable assignment looks like:

variable value <-

So, here's an example program for a fisher-yates algorithm implemented in this language:

{ arr :
  n  arr size <-
  0 n 2 - i [
    j    i n .. randin <-
    arr  arr [i j nswap] apply <-
  ] for
  arr isolate
} shuf def

You don't precisely need to know the specifics, but I want to point out three lines where variable assignment takes place:

  n  arr size <-
    j    i n .. randin <-
    arr  arr [i j nswap] apply <-

I've added whitespace to compensate what I think is a tad unreadable. However, having not utilized many readable postfix languages, I am not sure what is considered readable. So, I initially defined variable assignment so that it would more closely represent conventional variable assignment, like so:

my language:   a 4 <-
conventional:  a <- 4

However, upon actually writing some relative short programs, I feel like the readability could be improved by having the name adjacent to the assignment, in the same way how 2 3 * 4 + is more readable than 4 2 3 * +, since having only 2 items on the "stack of your mind" at a time allows you to evaluate it like a regular math problem. So, the program revised would be:

{ arr :
  arr size n <-
  0 n 2 - i [
    i n .. randin j <-
    arr [i j nswap] apply arr <-
  ] for
  arr isolate
} shuf def

Which seems to look more readable.

(Skip to the bottom for the actual question, I'm just explaining the original program in detail from here to there for those who want it.)

Alright, so a comment looks like (* ... *). Now, I've added comments explaining the program.

(* { name : ...} begins a lambda that takes a single argument
   `name` from the stack when executed. *)
{ arr :
  (* let n be the length of the array *)
  n  arr size <-
  (* from 0 to n - 2, using `i` as a variable... *)
  0   n 2 -   i
  (* ...execute a simple func, which has `i` in scope *)
  [
    (* set j to a random index, i <= j < n *)
    j  i n .. randin <-
    (* applies the stack operation `nswap` to the array,
       swapping elements `i` and `j`. *)
    arr arr [i j nswap] apply <-
  ] for
  (* make arr the only thing on the stack for a return value *)
  arr isolate
(* close the lambda and define as a function `shuf` *)
} shuf def

(* example usage *)
(1 2 3 4 5) shuf out

The question

In a postfix language such as this, would it be better style in the long run to have the variable name adjacent to the variable, or as the first argument? I chose the latter in hopes it would be more readable, but now it seems that the former is more readable.

(I realize I didn't gave the full specification for the language, and that's simply because I have none. Feel free to "define" your own examples for the sake of argument.)

  • 1
    @gnat That's not really the same thing. That's talking about a type in an infix language assignment, whilst this is talking about simple untyped variable assignment in a postfix language. – Conor O'Brien Dec 9 '16 at 14:24
  • 3
    My take is that once you're using postfix syntax, readability is already deader than disco, and no choice of subtle variants can revive it. – Kilian Foth Dec 9 '16 at 14:36
  • 1
    Voting to close this. I agree with @KilianFoth that by using postfix notation, you have rendered the code unreadable, so where variable names go is not only completely opinion-based (and so off-topic), it's a mute point too. – David Arno Dec 9 '16 at 14:39
  • 2
    @DavidArno Readability is all relative. All code is unreadable unless you know the language or a language similar. It's just a different style of coding that you are not familiar with, so it's not entirely to fair to declare it unreadable. – Conor O'Brien Dec 9 '16 at 15:09
  • 1
    @CᴏɴᴏʀO'Bʀɪᴇɴ, I disagree. That is not important though. It is off-topic as answers will be purely subjective. – David Arno Dec 9 '16 at 15:11
6

The appeal of a concatenative language is that the notation corresponds closely to a stack-based evaluation model: we have literals that are pushed onto the stack, and functions that may pop values off the stack and push values onto the stack. The instructions can be applied in order, i.e. this is effectively an assembly code for a stack based virtual machine.

Variables complicate this matter:

  • The storage represented by mutable variables is orthogonal to the stack model. Some might consider this impure. While reading from a variable fits cleanly into the stack model (a variable is a function that pops no values and pushes a value), writing does not.

  • Variable binding/assignment is necessarily syntax. Given three tokens a b c, the language would usually evaluate these tokens in order which results in stack manipulations. This cannot hold for variable assignment: if a or b is a variable, I would expect it to be evaluated. But instead, the assignment operator just needs an assignable name. The variable you are assigning to must not be evaluated. This means variable binding cannot be an ordinary stack function.

Since variable binding does not behave like an ordinary function, it should not look like an ordinary function. It's a very special, language-level operator.

Your solutions variable expression <- and start end variable expression for are actually circumfix operators: an assignment expression starts with a variable, then contains an ordinary stack expression, and ends with the assignment operator. I'd think this is a bit confusing since it spreads information about the assignment far apart. It is challenging to read such code since it is not at all obvious whether a given variable will be evaluated or assigned to unless you also read the rest of the program.

Your solutions expression variable <- and lambda variable def are a bit better since all information about the assignment is now closer together: When you encounter the variable <- instruction in the source code, the stack will contain the value to assign. But the assignment operator still starts with the variable where it looks like the variable should be evaluated.

I'd therefore recommend that assignment is an unary operator that contains the variable name directly, e.g. expression =variable or expression :variable. If there's whitespace in between that doesn't matter, even expression -> variable would be OK. This doesn't violate the postfix structure of the language because the variable is exempt from normal evaluation.

I'd expect that introducing a common variable assignment syntax would turn your example code in something like this (note that for the purpose of a simpler syntactic model I'll assume the for-loop takes a lambda as one parameter, which allows it to be an ordinary function instead of an operator:

{ :arr
  arr size :n
  { :i
    i n .. randin :j
    arr i j nswap :arr
  } 0 n 2 - for
  arr isolate
} :shuf
  • Oh, this is gold. I originally had variables be stored as references on the stack, but this simplifies my model. This makes the code much more readable. Thanks! – Conor O'Brien Dec 9 '16 at 15:45
  • Using this approach, how are array references assigned to, or any non-trivial expression as an lhs? – Erik Eidt Dec 9 '16 at 19:45
  • @ErikEidt well spotted, this strategy does not provide a general approach for lvalues, and can only handle local variables. In such languages, array accesses would have to go through (built-in) accessor functions, e.g. i arr array-get and value i arr array-set. We could also create a special case for arrays and an extra operator, e.g. value i :[]arr. Or we simply outlaw such memory manipulation and make such composite data immutable. Introducing lvalue expressions greatly complicates any language – we now have to think about memory models, const-ness, references, identity, …. – amon Dec 9 '16 at 20:01
  • @ErikEidt If it's of any interest, in the language I'm designing, there are designated get and set functions for arrays. – Conor O'Brien Dec 9 '16 at 20:10
2

As an example of a famous postfix language, Postscript Language Reference Manual (pdf) defines the assignment operator as following (p.568):

def           key value def -

associates key with value in the current dictionary—the one on the top of the dictionary stack (see Section 3.4, “Stacks”). If key is already present in the current dictionary, def simply replaces its value; otherwise, def creates a new entry for key and stores value with it.

Examples:

/ncnt 1 def           % Define ncnt to be 1 in current dict
/ncnt ncnt 1 add def  % ncnt now has value 2
  • A key point to note is that Postscript has the concept of a "name" or "symbol" as a value. In Forth, this was primarily handled by memory addresses (though the Forth dictionary worked similarly). The benefit of this approach is that it avoids introducing binding structure/local identifiers which is against the spirit of concatenative languages. The downside, of course, is nothing stops you from attempting to refer to "variables" that aren't "in scope". – Derek Elkins Dec 11 '16 at 21:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.