RFC: variable declaration syntax using `:=`

Yeah, it’s meant to indicate “this is a new variable in this scope”

x = 1 # or x := 1

# some code

if x := call
 # ^ error: variable `x` already declared in this scope
end
2 Likes

My only comment is that := looks so adequate for the original proposal of restricting type, that if you use it for this, and then decide to implement that, you’ll have lost a perfect operator.
I’d add a new keyword/syntax like var x, let x, new x, with x (?), maybe even |x|, for declarations.

:= is adequate syntax for a declaration without type restriction as well.
And I believe this is a far more common use case, so I’d rather use a different syntax for value-derived type restrictions.

I only know 3 cases, and in all of them it’s used for declaration with type inference + assignment:

  • Sather (probably the first to use this syntax): variable: type := expressionvariable ::= expression
  • Odin: variable: type = expressionvariable := expression
  • Go: variable type = expressionvariable := expression

Where is it used for just variable declaration?

Crystal is more lax in its type handling than other programming languages, in terms of variables are allowed to change type over time in more fluid ways. I do think there may be some confusion from people about := though, assuming it also restricts the type. Maybe ::= could be used for declaring a new variable and inferring the restricted type?

Then you could have:

class Foo
  @bar := "hello world"
  @baz ::= "goodnight moon"  

  def foo
    # this is fine
    @bar = :hello
    
    # this raises an error
    @baz = :goodnight
  end
end

Pascal for example uses the := operator for assignment and = is equality. Another kind of different semantics.

I don’t know if there is any prior art in that with exactly the behaviour I’m proposing.

But every language is different. Crystal has flow typing and that’s a pretty big deal. Does any of these other languages?
Due to type inference and flow typing, there is typically no need to explicitly declare the type local variables in Crystal.
There’s not even a need to declare them at all, just assigning will implicitly declare it.

The idea for := is to make declaring a variable explicit - as an optional feature - while keeping the regular flow typing behaviour.

@straight-shoota I understand for local variables, but how would the following behave, then? Same for class variables.

@ivar := value

Also the : denotes a type definition in Crystal, so it might get confusing.

The semantics of just declaring without type restriction is not very useful for instance or class variables. Their access isn’t sequential, and their type is fixed.

There isn’t much chance for accidental redefining.
I suppose there is a minor use case to prevent accidental reassignments

class Foo
  @foo = 1
  @foo = 2 # explicit declaration here would alert that it was already declared earlier
end
Foo.new

Currently, even an explicit type declaration on the second assignment is not an error. It just defines the type of the ivar (and it can be a wider type than the assignment).

class Foo
  @foo = 1
  @foo : Int32 = 2
end

For instance and class variables, the semantics of implicitly declaring a type restriction to the assigned value’s type could be useful as a shorthand notation.

Method parameters with default values are similar. There’s no need to ensure the variable hasn’t been declared before because parameter values are assigned in the prelude of a method. There couldn’t be a previous declaration.

But restricting the type of the parameter to the type of its default value could be a popular use case.

Without the explicit type restriction, bar’s type would be unrestricted. But it’s very common that the type of the default value should also serve as type restriction.

def foo(bar : String = "default")
end

Maybe we’re actually looking for two different behaviours? One for local variables, and on for class/instance variables and method parameters.

I suppose sharing the same syntax wouldn’t be a good idea :sweat_smile:

1 Like

I really don’t like := as an assignment without type restriction.

The original post started with how Odin does things, but there’s no variable declaration without type restriction in Odin, so there’s no direct comparison there.

A similar syntax that I also quite like is the way constants are declared:

my_name :: "Barney"

Maybe something similar could be used for variable (or maybe even constant in the future) declarations in Crystal?

Or maybe =: instead of := to handle declarations? Readability could become problematic though.

Ooooh, having := as a shorthand to infer type from simple value is a nice idea :eyes:

3 Likes

Just reading up on this.

def foo(bar := "default")
end

I think the above makes perfect sense as well and is in line with how variable types would work, no?

text := "Hello"

Or what do you consider the 2nd behaviour?

The question is whether := restricts the type of the variable to the type of the initial expression.
That was part of the original idea. But I believe it’s more common that you don’t want such a restriction in order to make good use of Crystal’s flow typing.

In the context of a method parameter, it’s quite common that the parameter’s type restriction is the type of the default value. But that only affects which types you can pass as argument.
It does not restrict the type of the local variable, like a type declaration would:

def foo(bar : String = "default")
  # bar is defined as a method parameter with type restriction
  bar = 1 # this is legal

  # baz is defined via type declaration
  baz : String = "default"
  baz = 1 # Error: type must be String, not (Int32 | String)
end

So I think there are actually three different concepts, which all could reasonably be associated with the := operator:

  1. Act as a type declaration. The variable is restricted to the type of the assigned value.
  2. Declare a variable, but not its type. Errors if the variable has been declared before. Only useful for local variables.
  3. Defer the type restriction of a method parameter from its default argument.

Actually, 3. also kinda depends on 1. vs 2. whether it acts as a type declaration (i.e. restricts the type of the variable). Currently, type restrictions on method parameters do not restrict the type of the variable, so it might be best for consistency to not restrict the variable.

1 Like

Oooh interesting. I actually did not know that parameter type restrictions do not restrict the type of the local variable.

With regards to := in this case, I view it as shorthand for : Type = in var : Type = type_instance, in which case using it in a parameter would be functionally the same as using it with a local variable.

That kind of begs the question though, how do you restrict the local variable then? Shouldn’t the parameter’s type be the type of the local var as well?

[quote=“Barney, post:54, topic:7623”]Shouldn’t the parameter’s type be the type of the local var as well?
[/quote]
The big difference is not the actual, current type itself, but whether the type is fixed or may change during the lifetime of the variable.
Flow typing is a central concept in Crystal. It makes the fully static typing feel very dynamic.
For that reason, type declarations (which pin the type) are typically rare in Crystal code bases.
There are even proposals to remove even more uses of them: Do not use `TypeDeclaration`s on local variables by HertzDevil · Pull Request #12364 · crystal-lang/crystal · GitHub

Considering the previous paragraphs, I’d counter the question with when or why would you even need to restrict the variable?

A practical answer of course is to declare a separate variable, independent of the parameter. This can be pretty transparent with an external parameter name:

def foo(bar bar_param : String = "default")
  bar : String = bar_param
  bar = 1 # Error: type must be String, not (Int32 | String)
end

An interesting point. I feel more secure explicitly fully typing a variable and knowing that I can’t accidentally change it :rofl:

But I probably wouldn’t go out of my way to restrict it like that :smiley:

AFAIK even if a parameter has an (illegal type) default value, it won’t report an error. the type of the default value seems to represent two different meanings from mandatory type declaration.

e.g. I thought the x is not limit to String type for current compile.

def foo(x = "hello")
end

foo(1) # => no error

following should be.

def foo(x := "hello")
end

foo(1) # => should raise compile-time error
1 Like