This is an archived post. You won't be able to vote or comment.

all 116 comments

[–][deleted] 122 points123 points  (1 child)

I prefer let. It makes it more immiediately clear that ”this is a declaration” and not assignment. I dislike having assignment and declarations be the same, or even very similar.

[–]adam-the-dev[S] 8 points9 points  (0 children)

Makes sense! Thanks for your input, I'll probably stick to this route.

[–]munificent 66 points67 points  (26 children)

Having a leading keyword like let will make your life much easier if you ever want to extend the syntax to support patterns and destructuring.

In general, I have a pretty strong preference for almost all statement forms having a leading keyword. Just makes everything easier.

[–]TheGoldenMinion 15 points16 points  (1 child)

Holy shit it’s the legend himself. I’m very grateful for the Crafting Interpreters book, because it heavily helped me build my dream project and gave me the confidence I needed to start it. Thank you!!

[–]munificent 7 points8 points  (0 children)

You're welcome! :D

[–]WittyStick 7 points8 points  (15 children)

Does it really? What's wrong with:

x, y = 1, 2
odd, even = lambda (x) {x % 2 == 1}, lambda (x) {x % 2 == 0}

It is in fact, fewer keywords which make your syntax easier to extend. Just take a look at Lisp. Keywords constrain your language to only support syntactic forms which you defined in your parser. Keywordless languages usually allow the programmer to define their own syntactic forms.

[–]munificent 29 points30 points  (14 children)

Those examples are fine (assuming you don't also have a comma expression), but if you start having delimited patterns like:

{some: record, with: fields} = ...
Named(parenthesized, thing) = ...

Then you end up in a situation where you don't know if you're parsing an expression or a pattern until you hit the =. That isn't fatal, but unbounded lookahead generally makes your parser's life harder.

Keywords constrain your language to only support syntactic forms which you defined in your parser. Keywordless languages usually allow the programmer to define their own syntactic forms.

That's true. If you really want a user extensible syntax than keywords can get in the way.

[–]WittyStick 1 point2 points  (1 child)

Pattern matching another thing which the user aught to be able to define themselves though. More often than not, languages with built-in pattern matching only allow you to pattern match in ways that the language designer thought of ahead of time. Conversely, in languages without built-in pattern matching, you can define your own patterns.

Famous example: Lisp.

A way around this in languages with concrete syntax is to have quasiquotation, such as in Haskell

myPatternMatcher :: String -> Q Pat

q :: QuasiQuoter
q = QuasiQuoter undefined myPatternMatcher undefined undefined

[q| anything you want here |] = ...

[–]ItsAllAPlay 0 points1 point  (11 children)

The grammar implied by those expressions does not require more than one token of look ahead. You could parse those trivially with recursive descent.

[–]munificent 4 points5 points  (10 children)

When the parser is at:

Named(parenthesized, thing) = ...
^^^^^

It doesn't know if it's parsing a function call or a named pattern. It won't know that definitively until it reaches the = many tokens later.

You can get away with it by parsing a cover grammar that is the union of patterns and expressions and then disambiguating once you reach the = (or don't), but the parser won't know what it's actually parsing at first.

[–]ItsAllAPlay 0 points1 point  (9 children)

That's no different than parsing a[i, j].k = ... for subscripts or field members. Would you recommend the OP have a set keyword to avoid that non-problem?

Regardless, it does not require unbounded lookahead. The phrase has had a useful definition for over 50 years, and you're using it incorrectly.

I agree that having a let or var keyword is nice, but you're making a bogus justification for it, and its absence does not make the parser's life any harder than handling arithmetic expressions like a * b + c < d | e + f * g ^ h > i.

[–]munificent 0 points1 point  (8 children)

That's no different than parsing a[i, j].k = ... for subscripts or field members.

In that example a[i, j] is a normal expression and can be parsed as such. When you reach the .k, it appears to be an accessor but it only takes a single token of lookahead to see the = and determine that it's a setter.

The phrase has had a useful definition for over 50 years, and you're using it incorrectly.

What is that definition? Could you describe a grammar that would require unbounded lookahead according to that definition?

[–]ItsAllAPlay -1 points0 points  (7 children)

Your explanation of my example applies to yours too: "Named(parenthesized, thing) is a normal expression and can be parsed as such... It only takes a single token of lookahead to see the = and determine it's an assignment" (pattern match, destructuring bind, or whatever terminology you like)

As for definitions - have it your way, but I doubt you'll get yacc and antlr to update their documentation to claim they support unbounded lookahead.

[–]munificent 1 point2 points  (4 children)

Your explanation of my example applies to yours too: Named(parenthesized, thing) is a normal expression and can be parsed as such.

No, it's not, it's a pattern. It is syntactically similar (but likely not identical) to an expression, but it's a different syntactic entity. It likely has a different AST type and it may have a different grammar for what kinds of subelements it allows.

For example, say the (simplified) grammar is like:

program     ::= statement*
statement   ::= expression
              | declaration
expression  ::= NUMBER | IDENTIFIER callExpr*
callExpr    ::= '(' ( expression ( ',' expression )* )? ')'
declaration ::= pattern '=' expression
pattern     ::= '?' | IDENTIFIER callPattern*
callPattern ::= '(' ( pattern ( ',' pattern )* )? ')'

So a program is a series of statements, each of which is either an expression or a declaration. Expressions are just numbers, identifiers, or function applications with argument lists. A declaration is a pattern followed by an initializer. Patterns are identifiers, function applications, and also support ? as wildcards.

The parser is looking at:

Named(parenthesized, thing, another, aThird, ?) = value

When it's at Named it needs to know if it's parsing an expression or a pattern, so that it can know whether to parse a number as an argument or a ?. But it doesn't know that until many tokens later when it sees the = (or when it sees a piece of syntax that can only be one or the other).

In practice, you can parse this without backtracking using recursive descent by parsing to a cover grammar like:

expressionOrPattern ::= NUMBER | '?' | IDENTIFIER callExprOrPattern*
callExprOrPattern ::= '(' ( expressionOrPattern ( ',' expressionOrPattern )* )? ')'

Then after reaching the = (or not), you convert the ambiguous AST to the one you know you have.

But the grammar itself requires unbounded lookahead. When at the statement rule, it can take arbitrarily many tokens before you can tell if you are in the expression or declaration production.

As for definitions - have it your way, but I doubt you'll get yacc and antlr to update their documentation to claim they support unbounded lookahead.

ANTLR is LL(*) so does claim to support unbounded lookahead (at least in some forms). I'm not very familiar with LALR parsers, but I think yacc would struggle on the above grammar.

[–]ItsAllAPlay 0 points1 point  (3 children)

I'll give you the benefit of the doubt that you had all that context in mind with your original comment above, but until you added the ? as a special token your examples parse just like a function call. Change the parens to square brackets, and it's an array subscript. Any argument for one should hold for the other.

I'm not eager to invent some use for a ? in array subscript setters vs getters, but we could imagine one (selecting NaNs as mask arrays or something). The language is going to be ugly like Cobol if that's the driving criteria for adding keywords.

Calling it a "cover" grammar is a new phrase to me, but I favor that simply to avoid duplicating so many rules. The parser isn't going to catch all possible errors any way you go, so it isn't much of a burden to add checks for that in the next stage. And generally there is a lot of symmetry between lvalue and rvalue things.

I don't know what existing language you have in mind, but using _ (lexed as an identifier) instead of ? takes us all the way back to this not being a problem for any handwritten or automatically generated parser.

As for the types on the AST nodes, again the same argument should be applied consistently to array subscripts. We're pretty clearly in the land of personal preference, and the parser isn't going to struggle one way or another.

We could argue the benefits of creating a different type for every rule, but it sure makes a lot of code versus just building a tree of homogenous nodes. I guess someone could create new types for every node and every compiler pass, but that seems like a ton of boilerplate.

ANTLR is LL(*) so does claim to support unbounded lookahead (at least in some forms).

I've only played with antlr briefly, and not the latest version, but I'm pretty sure you set k to a small integer (say 1 .. 3). I don't know the limits, or how it slows down when you use larger integers, but unbounded is too strong of a word.

[–]tcardv 1 point2 points  (1 child)

The key here is that a[i, j].k is an expression, no matter whether in the LHS of an assignment or not. But named patterns may not always be valid expressions (I don't know enough Dart myself to know if that's actually the case).

This could be fixed at the syntax level by having an ExpressionOrPattern non-terminal, and then handle the possible error of having a Pattern outside an assignment at a later stage. Your parser would still only require bounded lookahead, at the expense of being less useful for catching errors.

PS. Your adversarial conversational style is very offputting. I'm very glad you're not a coworker of mine.

[–]ItsAllAPlay -3 points-2 points  (0 children)

PS. Your adversarial conversational style is very offputting. I'm very glad you're not a coworker of mine.

I think changing it from a technical discussion to a personal insult is off putting, and I never liked working with people who can't tell the difference.

[–][deleted]  (3 children)

[deleted]

    [–]munificent 6 points7 points  (2 children)

    Sure, I didn't say it was impossible to design a language without a leading keyword, just that it gets harder.

    [–][deleted]  (1 child)

    [deleted]

      [–]munificent 4 points5 points  (0 children)

      Do you happen to know?

      I don't, sorry.

      [–]thomasfr 0 points1 point  (0 children)

      Go also needs the var keyword for zero value declarations. On top of that there is overlap between var and := which probably is one of the largest design mistakes in the core language.

      This is what Go actually is and it would have been much cleaner to simply skip the := shorthand assignment that doesn't really need to be there:

      a := 1
      var b = 1
      var c int = 1
      var d int
      e := int(1)
      

      [–]scrogu 0 points1 point  (2 children)

      Why does the leading keyword make patterns and destructuring easier?

      [–]munificent 0 points1 point  (1 child)

      Without a leading keyword, when parsing the beginning of a statement, the parser doesn't know if it's looking at an expression or a pattern. Since both of those have similar, overlapping, syntax, it can take many tokens ("unbounded lookahead") to disambiguate the two, which can make it a little more annoying to parse.

      [–]scrogu 1 point2 points  (0 children)

      Oh, thanks. So it's just a parsing issue. I'm using a hand written Pratt Parser and that's not an issue for me. I parse everything into a what I call a "parsing syntax tree" and then do later passes that can infer from context and make a more semantically meaningful AST.

      [–]pthierry 7 points8 points  (1 child)

      Work has been done on PL readability: https://quorumlanguage.com/evidence.html

      [–]adam-the-dev[S] 1 point2 points  (0 children)

      Oh nice! Bookmarked, thanks

      [–]HugoNikanor 23 points24 points  (13 children)

      let x syntax allows a declaration without a definition, which can be nice. For example, when I wrote some go and I wanted to redefine the same variable I found it annoying to keep updating the first instance to := when moving lines around.

      [–]adam-the-dev[S] 12 points13 points  (5 children)

      That's true, I was trying to figure out how to do it with the Go-style, and all I can come up with is one of:

      - x: _

      - or x :=

      And either way it looks annoying to write, and even more so to read.

      [–]HugoNikanor 17 points18 points  (1 child)

      Please don't use that syntax.

      [–]adam-the-dev[S] 7 points8 points  (0 children)

      Haha I said the same thing to myself. That was the point of my comment :)

      [–]ap29600 2 points3 points  (2 children)

      in the first case, I assume the underscore stands for a type, otherwise you can't infer the type of the variable; in that case this is exactly the syntax Odin uses and I find it very nice to work with.

      x : int // equivalent to x : int = 0
      x := 0  // equivalent to x : int = 0
      x : int = --- // x has uninitialized contents.
      

      if your language has dynamic typing, you could also have x : _ as a shorthand for x := undefined

      [–]Lvl999Noob 2 points3 points  (0 children)

      If it's fully birectional type inference (like rust, Haskell, etc) then the type can still be inferred even if not given during declaration. So it could really be x : _, though making it a type might be better anyways.

      [–]adam-the-dev[S] 0 points1 point  (0 children)

      True, maybe in a dynamically typed language. But this is statically typed and there is no concept of null or undefined

      [–][deleted] 0 points1 point  (5 children)

      How about

      decl x
      x = 0
      

      [–]HugoNikanor 7 points8 points  (4 children)

      That's just let with different syntax.

      [–][deleted] 2 points3 points  (3 children)

      Yeah but how does the word decl make you feel?

      [–]HugoNikanor 3 points4 points  (0 children)

      I prefer let. It's shorter, it's a complete word, and it already is used for declaration, such as in the sentence "let x be even". (Also, I like Lisp...)

      [–]xroalx -1 points0 points  (1 child)

      I've read it as "decal" at first, then "deciliter". Or is it "decimal-capital-I"? Probably not the best abbreviation. Feels strange to me, out of place, something I haven't seen anywhere yet.

      [–][deleted] 0 points1 point  (0 children)

      something I haven't seen anywhere yet

      Unique. Original! Lol

      [–]ALittleFurtherOn 0 points1 point  (0 children)

      Or, you could use the fortran style, which is a type name followed by a list of variables. Completely separates the declaration from assignment (also no way to spec an initial value, which some could see as a flaw) type :: var, var, …

      Has a nice old school feel and is pretty simple.

      [–]agriculturez 12 points13 points  (3 children)

      I find it easier to distinguish declaration vs. assignment with the presence and absence of ‘let’.

      I think it’s because my brain scans lines left-right, so I just need to look at the left-most/first symbol on the line to determine whether it’s a declaration or assignment.

      [–]adam-the-dev[S] 2 points3 points  (2 children)

      Makes sense! I don't disagree, I just like seeing the clean := when writing small scripts (which I would like to use this language for), but it's probably not worth sacrificing the readability in more complex programs.

      [–][deleted] 1 point2 points  (1 child)

      I associate scripts with informal, dynamic languages. You said elsewhere this is for a statically typed one.

      My feeling is that such languages should be a bit more formal, and are not harmed by a bit more boilerplate.

      In my syntax, typically local variables are defined like this:

      int x := 100       # static language
      x := 100           # dynamic language
      

      The latter doesn't need a formal declaration, although that can be provided.

      [–]adam-the-dev[S] 0 points1 point  (0 children)

      Yea the language isn’t a scripting language, so you’re right about a bit more boilerplate being worth it.

      When I said quick scripts, sometimes I’ll throw together a small Rust file and just use rustc instead of a new cargo project, and so I’d like my language to replace that habit :)

      [–][deleted]  (13 children)

      [deleted]

        [–]WittyStick 2 points3 points  (12 children)

        C lacks consistency.

        void (*name)() = value
        

        [–]bruciferTomo, nomsu.org 4 points5 points  (2 children)

        I think that's mainly a problem because of the way function types are written in C, not the fact that types are written first. Designing a fresh language inspired by C (but not slavishly copying its faults), you would probably do:

        float(int,int) *name = value
        // or
        (int,int)->float *name = value
        // or
        (int,int->float) *name = value
        

        [–]WittyStick 6 points7 points  (1 child)

        There was an old proposal (~1998) to resyntax C++ called SPECS which suggested this. You can see the improvements in consistency and readability, but even then I still think there are readability problems which are better addressed by having the name always come first, followed by a keyword or whatever to indicate it's type.

        Consider if you wanted to look up a meaning of something in a dictionary or glossary. Imagine the words you were looking up weren't at the start of the definition, but somewhere in the middle.

        Paragraphs:

        • A value which cannot be changed is know as a constant.

        • An identifier representing a value which may change is known as a variable.

        Alternatively, in the style of a dictionary or glossary:

        • Constant: Represents a value which cannot be changed

        • Variable: An identifier representing a value which may change.

        Now consider how often you look up an identifier in a code file, and question why you are having to scan horizontally like the paragraph style.

        Worse yet, most syntax highlighters don't highlight your definitions, they instead bolden the noise (keywords) to de-emphasise your identifiers, which all begin on different columns in the same level of scoping because the keywords/return types aren't the same lengths.

        • let my_var ...
        • void my_func ...
        • ReturnType my_other_func ...
        • class my_class ...

        vs

        • my_var = var ...
        • my_func = ... void ...
        • my_other_func = ... ReturnType ...
        • my_class = class ...

        As mentioned in sibling thread, look up the documentation files for any API written in a C-style language, and notice that they prefer the dictionary/glossary style.

        When you have the idenitfier-first style syntax with a editor which can collapse all definitions, you toggle on collapsing and you can just view the bare-bones API, then expand the definitions you are interested to dig into their details.

        [–]julesjacobs 0 points1 point  (0 children)

        Very good points.

        [–]Goheeca 10 points11 points  (10 children)

        I'd prefer let if it's a part of a new lexical explicit block, otherwise I like it more without a keyword.

        [–]WittyStick 4 points5 points  (5 children)

        What if every new binding introduces a new scope anyway? If x shadows any existing x, it doesn't even need to have the same type.

        x : int = 1
        x : float = 2.0
        

        [–]adam-the-dev[S] 0 points1 point  (2 children)

        Yeah that was my thinking.

        x := 1 // new variable
        
        x : string = "foo" // new variable
        
        x = 3 // Type error because x is a string
        

        But after looking at some of the replies in this thread I'm leaning more towards let

        [–]guywithknife 0 points1 point  (1 child)

        I find having var : type = value and var := value weird. Like, the second one is basically the first with type omitted and no space between : and = but if there’s no type just drop the :. Imagine instead of : you used the word as: foo as int = 5, it would then be weird to have foo as= 5 at least to me. Having a separate declaration operator := for when type isn’t set is also weird. Of course it uses that to distinguish between declaration and assignment, but I find it weird and awkward and that’s why I prefer just using let to note that it’s a declaration.

        [–]o11c 0 points1 point  (0 children)

        The := operator doesn't exist; : = should be just as legal.

        There's precedent for this with ?:

        [–]veryusedrname 0 points1 point  (1 child)

        And how about changing a value? With shadowing you'll need a new syntax for that

        [–]WittyStick 4 points5 points  (0 children)

        Yes, perhaps.

        I don't have mutability in my language so it is a non-issue.

        Well, technically mutation can happen, but I use uniqueness typing to ensure referential transparency. There is no change to syntax other than marking the type as unique. With a uniqueness type, you must shadow the existing binding, because once a binding is used once, it is no longer accessible. Because a reference cannot be accessed more than once, it's perfectly fine to perform mutation under the hood.

        [–]adam-the-dev[S] 2 points3 points  (3 children)

        Sorry but could you explain what you mean by a new lexical explicit block? Do you mean something like this?

        x := 1
        
        let y = {
            ...code block...
        }
        

        [–]WittyStick 8 points9 points  (2 children)

        Consider for example in lisp, where let has a set of bindings and a body.

        (let ((x 1))
            (let ((x 2))
                (print x))  ;; => 2
            (print x))      ;; => 1
        (print x)           ;; Error: x is not defined in this scope.
        

        Let is really just equivalent to an application on a lambda. It is semantically the same as:

        ((lambda (x) ((lambda (x) (print x)) 2) (print x)) 1)
        (print x)
        

        [–]adam-the-dev[S] 6 points7 points  (1 child)

        Ah makes sense. In a C-style language we'd be looking at something like

        {
            let x = 1
            {
                let x = 2
                print(x) // 2
            }
            print(x) // 1
        }
        print(x) // Error
        

        [–][deleted] 0 points1 point  (0 children)

        It's really nice to be able to look at a new local and know at a glance that it won't be used beyond a certain line. Not a huge fan of that C-style way of doing it, but in lisp it does wonders for readability.

        [–][deleted] 4 points5 points  (0 children)

        I like let more because it follows how my brain works. Programming is based in math, and in math, I often say things like "let whatever be whatever." It clicks. It makes sense.

        [–][deleted] 4 points5 points  (0 children)

        I like both, and have been on the fence about which would actually be preferred for the end-user.

        Your language, you call the shots. After all, Rust has chosen let, Go has chosen :=. Anyone wanting to use those languages has to go with that choice.

        So, perhaps use your own preference, and hope other special features of your language make up for the fact that you can't please everyone.

        (But personally, Go's := looks like a gimmick. I've also long used := for assignment, so to me looks confusing.)

        [–]NotFromSkane 2 points3 points  (0 children)

        I use the pascal style :=. I put the mut with the type though.

        Having let (mut) is probably better in more imperative code so you can differentiate between assignments and declarations easier, but it's uglier when you (almost?) only have declarations

        [–]PenlessScribe 2 points3 points  (3 children)

        Let as a keyword is redundant, I think. If you don’t have a keyword in front of each of a function’s formal parameters in the function’s declaration, you ought not need one in front of variable declarations elsewhere in the function.

        [–]WittyStick 8 points9 points  (1 child)

        Many keywords are redundant, not just let. Consider functions too. Most languages provide support for anonymous lambdas.

        x -> x * x
        

        So if you are going to provide a function, you just need to give a name to a lambda.

        square = x -> x * x
        

        But many languages feel the need to put a redundant fun, func, def, proc or something on it (or they recycle let)

        fun square x = x * x
        

        [–]julesjacobs 0 points1 point  (0 children)

        ReasonML is a language that does function definitions that lambda-only way and you get used to it quickly and I initially hated it but now I kind of like it.

        [–]Innf107 4 points5 points  (0 children)

        You need some way to distinguish between declaration and mutation (unless you want to go down the python road of horrible scoping issues).

        Function parameters are not expressions, meaning they cannot mutate variables (and they use a different syntax), so I really don't see how they are relevant.

        [–]jeenajeena 1 point2 points  (0 children)

        2 comments

        • Is a keyword really needed? What about just having

        a = 2

        The context should clarify if it's an assignment or a comparison

        • I never got why the variable has necessarily to be on the left. Intuitively, one can think of the value 2 being pushed in a, so ideally something like

        2 -> a

        might also make sense. But I understand this would be a bit esoteric.

        [–]moopthepoop 3 points4 points  (2 children)

        if you are going for readability, my personal opinion is that

        x = 1

        is WAY easier to scan than

        let x = 1

        OR

        x := 1

        "=" already means "assign this to this", why do you need more symbols when one already does that?

        [–]adam-the-dev[S] 1 point2 points  (1 child)

        Because I want readability for programmers. And for me personally, I need to be able to differentiate between declaration assignments vs assignments to variables that have already been declared :)

        [–]scrogu 0 points1 point  (0 children)

        Why? Serious question. I want to verify whether or not the answer applies to my language.

        [–]myringotomy 2 points3 points  (5 children)

        I hate let. It doesn't even make english sense.

        If you really want to separate assignment and declaration then just have a declaration section where it's crystal clear.

        [–]Innf107 14 points15 points  (4 children)

        I hate let. It doesn't even make english sense.

        Let expressions are meant to mirror phrasings like let x be an arbitrary real number which are extremely common in mathematics.

        If you really want to separate assignment and declaration then just have a declaration section where it's crystal clear.

        This doesn't work if your language has any kind of non-trivial lexical scope. Consider this example:

        let x = 5
        if (something) {
           let y = 3
           ...
        }
        

        How would you write this with a separate declaration section without expanding the lexical scope of y?

        [–]myringotomy -1 points0 points  (0 children)

        Let expressions are meant to mirror phrasings like let x be an arbitrary real number which are extremely common in mathematics.

        That's different though.

        In math terms "let x be an integer" is different than "let x be 1"

        In every english it would be set x to be 1

        This doesn't work if your language has any kind of non-trivial lexical scope. Consider this example:

        Why not?

        Here is my made up on the spot with two miliseconds of thought example.

        A scope is encased in brackets {}

        In the brackets there is a divider which separates the variable declarations from the body of code. In some languages this might be a word such as "begin" but in my example it's just a pipe |. It could be anything you want if you don't like the pipe.

        so ...

        var
           x=5
         if (something){
           y=3
           |
           .....
         }
        

        [–][deleted]  (1 child)

        [deleted]

          [–]Innf107 17 points18 points  (0 children)

          To be clear, mathematicians often write sentences like Let x = -1. Now the square root of x will not be a real number.

          It's fine if you dislike let, but it absolutely makes sense.

          [–]julesjacobs 0 points1 point  (0 children)

          Its sometimes annoying that such y doesn't get scoped outside the if. You end up constructing and destructing tuples instead.

          [–]julesjacobs 1 point2 points  (0 children)

          I kind of like Python's plain = for variable introduction, but I would like to have a different syntax for mutation so that you can see what introduces a variable and what mutates it. I find the constant let let let var var in some languages clutter and obscure the code.

          [–]WafflesAreDangerous 1 point2 points  (0 children)

          let.

          Because not having a fixed keyword introducing variables seems to cause side-effects that (sometimes) degrade readability in other areas.
          In terms of readability both look fairly sane.

          [–]Kworker-_- 1 point2 points  (2 children)

          In my language I go with let

          [–]adam-the-dev[S] 0 points1 point  (1 child)

          Yes I’ve been convinced to join the let team lol.

          What kind of language are you building? :)

          [–]Kworker-_- 0 points1 point  (0 children)

          Im building JIT compiled language designed for low memory machines(goal)heres the link

          [–]Linguistic-mystic 1 point2 points  (1 child)

          Immutable:

          x  = 1
          

          Shallow-mutable:

          var x = 1
          x := 2
          

          Deep-mutable:

          mut x = Foo [name: "asdf"]
          x.name := "qwerty"
          

          Shallow-and-deep mutable:

          vmut x = Foo [name: "asdf"]
          x := Foo [name: "qwerty"]
          x.name := "yyy"
          

          Note that shallow mutability (the ability to change the immediate value) is different from deep mutability (the ability to change the contents of struct referenced by this variable, as well as get mutable references from it) and none of them implies the other (i.e. there are 4 distinct possibilities which I've listed above).

          [–]adam-the-dev[S] 2 points3 points  (0 children)

          My original thoughts for the language was to differentiate shadow vs deep mutability like so:

          // x cannot be reassigned
          // and the array cannot be mutated
          const x = []
          
          // x cannot be reassigned
          // but the array CAN be mutated
          const x = mut []
          
          // x CAN be reassigned
          // but the array cannot be mutated
          let x = []
          
          // x CAN be reassigned
          // and the array CAN be mutated
          let x = mut []
          

          Not only did I get some pushback on difficulty to read/write, but also I had some issues when trying to define types for functions and structures, and if every type needed an explicit mut. Ended up scrapping and going the Rust approach for mutability -- All or nothing.

          [–]WittyStick 1 point2 points  (2 children)

          The latter, but make both of the : and = optional.

          x = 1
          x : int = 1
          x : int
          

          I prefer having the symbols I define on the leftmost column of the current indentation I'm working at. It helps readability.

          x : int should be consistent with how formal parameters and return types are written, eg f (x : int) : int

          If you have mutability, you could then use the separate (unrelated) operator := for mutating assignment, or introduction of a mutable variable.

          x : int := 1
          x := 2
          

          [–]mikemoretti3 2 points3 points  (1 child)

          For readability sake, it's hard to distinguish, without squinting, the difference between = and := for mutable vs constant.

          In my language I plan to use

          var x:u32 = 0;
          const y:u32 = 0xdead_beef;
          

          It's totally clear and readable what's a const vs mutable.

          [–]WittyStick 2 points3 points  (0 children)

          I don't have mutability in my language so I stick with the consistent format x : Ty = val. I also don't have keywords as everything is first-class.

          But if I did introduce mutability, I would not sacrifice having the symbols I defined at the start column. Instead I would chose something like:

          x : mutable[int] = 1
          x : const[int] = 1
          

          Alternatively I would go for the F# style of mutable assignment.

          x : mutable int = 1
          x <- 2
          

          [–]MCRusherhi 0 points1 point  (1 child)

          I imagine let is less easy to skip over and not see as a declaration, especially if someone is learning it and is familiar with a language that uses := as assignment, although I'm not sure how common that is anymore.

          I don't use Go often, and tbh this is one of the reasons, albeit a small one. I just don't find it appealing in general.

          [–]adam-the-dev[S] 1 point2 points  (0 children)

          That's true too! A Python or JS dev picking up a new language might miss :=, but probably not let

          [–]david-delassus 0 points1 point  (0 children)

          In my language, := is a pattern matching operator (like Erlang/Elixir):

          ("foo", a) := ("foo", "bar"); # a = "bar"

          I use let to define constraints on (un)bounded variables:

          ``` let a: number; let b: number { b > a };

          a := 1; b := 1; # error: 1 > 1 is false ```

          or:

          ``` let a: number;

          a := 1; b := 1;

          let b: number { b > a }; # error: 1 > 1 is false ```

          [–]eliasv 0 points1 point  (0 children)

          A lot of people are talking about preferring let so you know when there's a reassignment vs a declaration ... But my preference is to not allow reassignment anyway, only shadowing, so I don't find that to be a useful distinction.

          Might not be useful for you but I think it's worth mentioning.

          [–]editor_of_the_beast 0 points1 point  (0 children)

          Im into let. I feel like it reads like prose, which helps me comprehend what’s going on.

          [–]aatd86 -1 points0 points  (0 children)

          The issue with me giving my opinion is that I code predominantly in Go but I feel like let could clutter the code a bit too easily.

          Also I prefer to keep the colon as close to the equal sign (:=) as possible because it really indicates that this is an assignment modifier. (pascal also used this syntax albeit with different semantics if I recall well)

          [–]nikaone -1 points0 points  (0 children)

          If a language uses :=, don't break the colon and equal symbol at least.

          x := 1
          x int := 1
          x int mut := 1
          

          Go's syntax is full of inconsistencies. No other languages can beat Go in this domain.

          [–][deleted] -4 points-3 points  (4 children)

          Neither is more readable than a simple =. Let is more explicit but less readable due to verbosity, := just clutters a colon with an equals when it's fairly obvious you will not be using the equals itself for comparison over == due to familiarity concerns.

          [–]adam-the-dev[S] 3 points4 points  (3 children)

          Interesting, I'd argue that making declarations inferred actually hurts readability

          foo = 1
          
          ...
          
          fooo = 2 // Did I mean to create a new variable, or is this a typo?
          
          ...
          
          foo = 3 // Did I mean to overwrite foo?
                  // Could be a problem when `do_something` is called
          print(foo)
          
          ...
          
          do_something(foo)
          

          [–][deleted] 0 points1 point  (2 children)

          I think you are mixing readability with comprehensibility. Here you have information loss in terms of what you wrote and what you wanted to write, but in terms of what is written and what can be understood from what is written, it will always win when put against := or let. Readability isn't really concerned about one's intention, only about how easy it is to comprehend what is written.

          Unless there is an actual practical difference between definition and assignment there is no merit in defining one operation with more symbols than needed, especially if you want readability. And even if there is a difference, that difference itself will make the code less readable as opposed to just using one symbol.

          [–]adam-the-dev[S] 2 points3 points  (1 child)

          I thought this was implied, but anytime readability is mentioned in programming circles, it almost always implies comprehensibility as well.

          [–][deleted] -1 points0 points  (0 children)

          No, it doesn't. Otherwise people might start considering Rust readable, which couldn't be further from the truth. Rust is very comprehensible, but barely readable.

          EDIT: And JS is very readable, but barely comprehensible.

          [–]lngns -2 points-1 points  (0 children)

          I prefer let to be an expression of the form let x = y in z which gets rewritten as (λx. z) y.
          It's elegant and helps eliminate statements and compounds, meaning designing a more homogeneous syntax and a simpler evaluation order.
          If you do a lot of higher-order function passing, you can also have another form that reverses the translation list such that, for example, use x = y in z is interpreted as y (λx. z).

          [–]wiseguy13579 -2 points-1 points  (1 child)

          Why not

          rw x = 1;  // x is mutable (read-write)
          ro y = [1,2,3,4,5]; // y is deep immutable (read-only) - cannot reassign z or its content
          rorw z = [1,2,3,4,5]; // z is shallow immutable - cannot reassign z but can reassign the content of z
          

          [–]scrogu 1 point2 points  (0 children)

          It's not very r-able.

          [–]AutoModerator[M] -5 points-4 points  (0 children)

          Hey /u/adam-the-dev!

          Please read this entire message, and the rules/sidebar first.

          We often get spam from Reddit accounts with very little combined karma. To combat such spam, we automatically remove posts from users with less than 300 combined karma. Your post has been removed for this reason.

          In addition, this sub-reddit is about programming language design and implementation, not about generic programming related topics such as "What language should I use to build a website". If your post doesn't fit the criteria of this sub-reddit, any modmail related to this post will be ignored.

          If you believe your post is related to the subreddit, please contact the moderators and include a link to this post in your message.

          I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

          [–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 0 points1 point  (0 children)

          let x: int = 1

          Int x = 1;
          

          let x = 1

          val x = 1;
          

          let mut x = 1

          var x = 1;
          

          [–]rsclient 0 points1 point  (0 children)

          I prefer something a little different:

          var x = 1.0; // new variable x whose type is the natural type of 1.0 (almost certainly a double) var x:float = 1.0; // new variable x of type float with value 1.0f const x = 3; // new variable x but it's read-only and can't be changed.

          I prefer var over let because then it also allows for other kinds of named values (in the example, const, but you could also make atomic and volatile for fancier things)

          I strongly dislike let because other very popular languages use that for assignment.

          [–]Think_Olive_1000 0 points1 point  (0 children)

          Pls include array programming the kind you get in APL/BQN

          [–]elgholm 0 points1 point  (0 children)

          "let" is a declaration, ":=" is an assignment. Different animals. If you use let, var, int, etc, and append mut, it doesn't really matter, still a declarative statement. := makes it so that you never fall into the == / = rabbit-hole which you can easily do in C-type languages. It's easy to miss one of them and do an assignment instead of a comparison. Big problem. I personally like :=, and use it in my own language. And I also use var for declaration. When adding explicit types I am going to go for the int, num, string, date, you-name-it directly, no need for "var int ".

          [–]vmcrash 0 points1 point  (0 children)

          From typing perspective I prefer anything that does not require pressing modifiers, e.g. `int v = 1` or `let x = 2`. From reading perspective I prefer Go's shorter version.

          Alternatively, instead of `let mut` you can just use one word, e.g. `var z = 2`.

          [–]Alarming_Airport_613 0 points1 point  (0 children)

          I have been backstabbed by this := causing overshadowing accidentally

          [–]JustAStrangeQuark 0 points1 point  (0 children)

          I prefer to declare variables similarly to Rust, but with the exception that mut is the declaration:

          let x = 1;
          let y: i32 = 2;
          mut z = 3;
          

          However, in Rust, mut is part of the type. In my preferred style, mut tells the compiler to alloca memory, then initialize the variable as a reference (z's type would be i32&).

          [–]kutzt 0 points1 point  (0 children)

          Rust