RFC: variable declaration syntax using `:=`

That’s completely understandable. While Crystal is pretty sweet as-is, need to be careful about not making the language too sweet :slightly_smiling_face:.

There are definitely higher priorities than this, just thought it would be useful to explore different ways of embedding developer intent into the source code, as it helps to stave off unexpected behavior and bugs.

Long soliloquy incoming, I apologise.

they can be quite burdensome for newcomers

As someone who started using crystal relatively extensively as a hobby coder last year, with very little prior coding experience, I think I’m uniquely qualified to chime in on this :DD

As you pointed out, having dozens of different notations, keywords etc is really challenging for people starting out, particularly with little coding experience, because it increases the complexity significantly.

With that being said, the Crystal team has done a phenomenal job with the language. At no point did I feel like syntax sugar was introduced just for the sake of “being cool”. There was always a clear intent and reasonable thought process behind it. The language has been a joy to learn and this intuitive syntax sugar is what makes it so fun for me.

In that same vein, := follows a very logical derivation of the underlying syntax.

# Define a variable with inferred, open Type
x = 5 # x is Int32, but can change.

# Define a variable with fixed, explicit Type
x : Int32 = 5 # x is Int32 and cannot change.

# Based on this, any of the following are logical,
# intuitive steps for the inferred, yet fix Type
x : _ = 5
x : = 5
x := 5

None of the above are are pulled out of thin air, and anyone starting out with the language can see how it’s derived with relative ease.

TL;DR: I think Crystal is the best language precisely because it has a lot of syntax sugar, but they’re all very intuitive, easy to learn and convenient to use.

7 Likes

:heart:

1 Like

That’s a really good point :ok_hand:

4 Likes

This syntax is interesting, and I think writing it myself would be enjoyable.

However, if a library used this syntax, I might not like the code very much.

In my opinion, managing types too strictly is not always helpful for library users or those who want to reuse the code. While specifying every type might reduce mistakes for developers, it can make the library less flexible and harder to use when small type differences occur.

For others reading the code, it’s easier to understand if types are written explicitly when they need to be specified.

This might just be my hesitation toward new things. I won’t know until I actually try using it…

Using a compiler-backed annotation is also possible. I don’t think a new syntax is necessary here.

Could you elaborate on that?

As RFC: variable declaration syntax using `:=` - #22 by Barney demonstrates, the proposal isn’t completely new syntax, but a derivate of existing syntax.
So I think the level of barrier is lower than for introducing a completely new syntactical concept.

While I don’t think this would be an issue for what you’re talking about with local, instance, and class vars (the first not being something you’ll interact with much in a library, and the latter only allowing type changes within the classes they’re defined), I do see how this could be a problem for parameters:

def to_s(io := STDIN)
  io << "hello world"
end

io would be inferred to be IO::FileDescriptor instead of the general IO class, and not allow any other types that would usually fit (like IO::Memory, etc). I think this is just part of the tradeoff / balance between duck typing and full typing, where duck typing can be nice and allows more flexibility, but full typing allows for better semantic errors and can make code easier to understand. I don’t think this is a dealbreaker though, just something library developers should keep in mind.

5 Likes

Making it harder to write code, like requiring types to be written explicitly, is a way to make everyone write readable and consistent code. This is more common in Python than in Ruby. In Ruby, Matz prefers to give users freedom and often says, “With great power comes great responsibility.”

I think this syntax is interesting, and I would probably use it a lot if it were added. But I wonder if this would really make the code easier for others to read or reuse. If the community develops good practices, it might not be a problem. If this syntax is introduced, the community will probably discuss how to use it in the best way.

1 Like

That’s completely fair. Using := in documentation may have things show up with typeof(...), which is definitely not as readable as listing the type explicitly.

Ameba 1.7.0 will have rules to enforce method parameter / return type restrictions and type restrictions for getter / setter / property calls (off by default), which should help the community push more towards explicitly typing things.

4 Likes

I dont think that should happen.

Granted, this is my hacky implementation that is just syntactic sugar for : typeof(...), but this is what happens currently on my branch:

class MyClass
  getter name := "George"
end

If by “should” you mean that the behavior of the docs generator should be that the typeof(...) is resolved to the actual type, fair enough, that should be possible / straightforward

Ah no I guess it won’t be straightforward because it would require semantic analysis for resolving the typeof. The doc generator doesn’t do that.

I really like this too!, if we add :=, i thought it can do far more than expected, need more discussion.

Want to mention cr-source-typer shards here, discuss:

And the efforts for try to add into Crystal, PR

It can add all types for method args and return value automatically, I really like it when write Crystal shard

2 Likes

Thanks for the shout out! Due to the holidays and life in general, that PR has been languishing lately, and I’m running out of improvements to do for it (also may be falling into that trap of infinite polish that devs sometimes do…). I’ll get that PR out of draft state and subject it to the masses officially :)

I’m really glad to hear that ameba will be adding some linting rules to help with this effort, too. A lot of benefits for explicitly typing everything for even mildly complex projects.

2 Likes

I’ve recently had a bit of a mishap where I accidentally redefined a local variable in the same scope and that didn’t go well.

That reminds me that it would be a great help if we had a simple syntax feature for that.

And it also made me realize that for me, the primary feature is declaring a new variable to avoid accidental shadowing.

But that really doesn’t need to involve type restrictions. It’s totally legit and very common to assign a value of one type to a variable, and later a value of a different type.
The compiler can figure out the exact type of the variable every time it is accessed.
Crystal’s flow typing is an awesome feature that makes it possible to write very concise code with type safety.

Static type restrictions cannot adequately reflect that.
Restricting the variable’s type to at declaration is impractical for a great many use cases. The compiler can figure things out, and it’s best to just let it do that.

In essence, I think the := operator should not be concerned with typing. Its only concern should be if a variable of the same name has already been declared in the current scope.

I imagine this could be used very widely. Ideally, every first assignment to a variable should be a declaration. This could perhaps even be supported by the formatter or linter.
The point is, using it everywhere should just means replacing = with := where appropriate. You should not have to worry this substitution could end up in a type error further down because the type of the initial value is too restrictive.

3 Likes

I’m not familiar with this syntax, but this sounds neat.

Would this sort of thing also handle block variables?

x := 1

things.each do |x|
  # does the compiler bork here?
end

I think ameba even yells for shadowing outer variable like this.

I don’t think that would be an issue, but something like this could be:

things.each do |x|
  x := 1
  # ^^^^ error: variable `x` already declared
end

oh, I see. So it’s meant for catching when you’ve already declared the variable as opposed to catching other variables declared later?

# this is ok?
x := 1
x = "two"

# but this is where it would catch?
x = "two"
x := 1

Or do I have that wrong and both would be caught by this?

1 Like