# RFC: variable declaration syntax using \`:=\`

**URL:** <https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623>\
**Category:** Crystal Contrib\
**Tags:** rfc\
**Created:** [January 20, 2025, 8:30am UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623 "2025-01-20T08:30:55Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 8:30am UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/1 "2025-01-20T08:30:55Z")

</div>

Currently, declaring vars happens either transparently through a normal assign (but with the downside of allowing the type to be flexible), or via a type declaration (which requires specifying the type explicitly).

I think it could be useful to pull in the variable declaration syntax from Odin (`:=`), with the behavior that the type will be inferred, but cannot be changed from whatever the type is originally inferred to be. I think this could be clearer when reading code the intention of the original developer, and can prevent subtle issues (like accidentally reusing the same variable within the same scope).

```crystal
class MyClass
  @my_ivar = 1

  def update(float : Float64)
    # oops the type of @my_ivar is now (Int32 | Float64),
    # and this can happen anywhere
    @my_ivar = float
  end
end

```

vs

```crystal
class MyClass
  @my_ivar := 1

  def update(float : Float64)
    @my_ivar = float
    # ^^^^^^^^^^^^^^ error: `@my_ivar` inferred to be type `Int32`, not `Float64`
  end
end

```

As well, trying to re-declare the same variable should produce a syntax / semantic error:

```crystal
my_var := 1

# bunches of lines of code

if my_var := read_string
   # ^^^^^^^ error: Cannot redeclare variable `my_var`
end

```

---

<div class="post-metadata">

**Author:** ![Barney](https://avatars.discourse-cdn.com/v4/letter/b/48db29/32.png) [@Barney](https://forum.crystal-lang.org/u/Barney)\
**Post date:** [January 20, 2025, 8:46am UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/2 "2025-01-20T08:46:56Z")

</div>

I really like this!!

I often find myself preferring explicitly typed variables, but that is a bit bothersome currently.

Futhermore, explicit typing for user-made classes etc. can get a bit unwieldy, even more so if modules are involved.

```cr
class Pokemon
end

snorlax = Pokemon.new
snorlax = 6 # Valid

jigglypuff : Pokemon = Pokemon.new
jigglypuff = 6 # Compile time error

```

The proposed addition would allow us to leave the type restriction to the compiler

```cr
class Pokemon
end

jigglypuff := Pokemon.new # Type is Pokemon

```

---

<div class="post-metadata">

**Author:** ![bcardiff](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/bcardiff/32/3_2.png) [@bcardiff](https://forum.crystal-lang.org/u/bcardiff)\
**Post date:** [January 20, 2025, 10:44am UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/3 "2025-01-20T10:44:56Z")

</div>

To pin the type of a local variable you can use

```auto
v : String
v = 1
# Error: type must be String, not Int32

```

ivars are slightly pined already by the type inference rules.

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [January 20, 2025, 10:46am UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/4 "2025-01-20T10:46:03Z")

</div>

I suppose `x := a` could perhaps be implemented as syntax sugar for `x : typeof(a) = a`.

You can experiment with that using a macro:

```cr
macro let(assign)
  {{ assign.target }} : typeof({{assign.value}}) = {{ assign.value }}
end

let x = 1
x = "foo" # Error: type must be Int32, not (Int32 | String)

```

Note: This won’t work for ivars though because `typeof` is not allowed for ivar type declarations (maybe it should?). So this currently cannot satisfy the first example.

Maybe such a macro could also be an alternative syntax instead of `:=` which would make implementation not depend on compiler support?

---

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 2:30pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/5 "2025-01-20T14:30:02Z")

</div>

The benefit of this is not needing to explicitly specify the type while also restricting it to that type.

Ivars are restricted from changes externally, but can be changed within the same class unless the restriction is specified.

---

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 2:38pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/6 "2025-01-20T14:38:54Z")

</div>

That’s exactly it! I think that’d work perfectly. I personally prefer `:=` to `let` as it’d be cleaner when doing `getter`:

```crystal
class MyClass
  getter name := "George"
  getter let age = 27
end

```

There is the benefit of `let` not requiring parser changes though.

Would that macro call the value twice? Maybe slight improvement:

```crystal
macro let(assign)
  %value = {{ assign.value }}
  {{ assign.target }} : typeof(%value) = %value
end

let x = 1
x = "foo" # Error: ...

```

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [January 20, 2025, 2:45pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/7 "2025-01-20T14:45:02Z")

</div>

Yes, your variant with caching the value in a local variable should be better. I originally omitted that for brevity.

As I mentioned above, this macro won’t work with instance variables, so at the moment it can’t be combined with `getter` either.

---

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 3:04pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/8 "2025-01-20T15:04:58Z")

</div>

Ah! Disregard my “improvement”, the expressions inside `typeof` aren’t evaluated, only checked for their return types, so you’re original example would work fine. [typeof - Crystal](https://crystal-lang.org/reference/1.15/syntax_and_semantics/typeof.html)

I couldn’t find any issues about typeof initialising instance vars with a quick search, but I imagine it could be problematic if you did this:

```crystal
class MyClass
  @ivar1 : typeof(@ivar2)
  @ivar2 : typeof(@ivar1)
end

```

Maybe the compiler could do a best-effort to evaluate the type, but if it can’t then throw an error(?).

---

<div class="post-metadata">

**Author:** ![Barney](https://avatars.discourse-cdn.com/v4/letter/b/48db29/32.png) [@Barney](https://forum.crystal-lang.org/u/Barney)\
**Post date:** [January 20, 2025, 3:29pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/9 "2025-01-20T15:29:36Z")

</div>

For what my opinion is worth, I think the

```cr
class MyClass
  getter name := "George"
end

```

syntax is very smexy and reads super well. I’d prefer it over `let` too.

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [January 20, 2025, 4:37pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/10 "2025-01-20T16:37:22Z")

</div>

The problem is that the compiler doesn’t allow `typeof` in a type declaration for an instance variable:

```cr
class Foo
  @var : typeof(1) # Error: can't use 'typeof' here
end

```

---

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 4:38pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/11 "2025-01-20T16:38:58Z")

</div>

That’s the problem I was referring to - maybe it’d be feasible to change that to allow for `typeof()` for instance vars, but in a limited capacity, and I can see why they were disabled.

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [January 20, 2025, 4:39pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/12 "2025-01-20T16:39:00Z")

</div>

The original macro example would not execute duplicate code, but the compiler would still have to type the same code twice. Storing the value in a fresh local variable allows it to reuse the result of semantic analysis. This can be relevant when the value is a complex expression (instead of a simple literal for example).

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [January 20, 2025, 4:47pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/13 "2025-01-20T16:47:22Z")

</div>

> [@nobodywasishere](#):
>
> I can see why they were disabled.

I’m wouldn’t even be quite sure whether that’s an intentional limitation. I don’t see why it shouldn’t be possible. Sure some expressions might not be able to produce a valid type, but that’s true for `typeof` anywhere.

---

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 5:02pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/14 "2025-01-20T17:02:37Z")

</div>

`typeof()` also can’t be used in parameter type restrictions:

```crystal
In test.cr:4:15

 4 | def check(var := :symbol)
                   ^
Error: can't use typeof in type restrictions

```

---

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 5:33pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/15 "2025-01-20T17:33:30Z")

</div>

Started experimenting with this on a branch ([GitHub - nobodywasishere/crystal at nobody/var-declaration-experiment](https://github.com/nobodywasishere/crystal/tree/nobody/var-declaration-experiment)) just to see what’s feasible and get a feel for the syntax.

---

<div class="post-metadata">

**Author:** ![keidax](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/keidax/32/2729_2.png) [@keidax](https://forum.crystal-lang.org/u/keidax)\
**Post date:** [January 20, 2025, 7:25pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/16 "2025-01-20T19:25:09Z")

</div>

Just pointing out this is also [a feature in Golang](https://stackoverflow.com/a/45654233). It might be worth discussing some of the nuances of where the `:=` operator is valid, like multi-assignment.

I would expect this to be an error, for instance.

```crystal
if a := some_function
  a = func_with_different_type # compile type error
end

```

Go’s treatment of multi-assignment with `:=` is interesting, because existing variables can be re-assigned but not re-declared as long as at least one new variable exists on the left hand side:

```go
func main() {
	a, b := 0, 0

	a, b, c := 1, 2, "c"
	// same as
	a, b = 1, 2
	c := "c"
}

```

---

<div class="post-metadata">

**Author:** ![Barney](https://avatars.discourse-cdn.com/v4/letter/b/48db29/32.png) [@Barney](https://forum.crystal-lang.org/u/Barney)\
**Post date:** [January 20, 2025, 9:02pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/17 "2025-01-20T21:02:45Z")

</div>

That is interesting. Odin does not allow that. `:=` is uniquely a declaration, which I think makes more sense than the hybrid approach that golang follows

```odin
main :: proc() {
    a, b := 2, 3
    a, b, c := 1, 2, "c" // Error due to a redeclaration of 'a' and 'b'
}

```

Following up on that, IMO assignments should never use `:=`, only `=`.

---

<div class="post-metadata">

**Author:** ![keidax](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/keidax/32/2729_2.png) [@keidax](https://forum.crystal-lang.org/u/keidax)\
**Post date:** [January 20, 2025, 9:24pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/18 "2025-01-20T21:24:06Z")

</div>

I think the main reason Go allows reassignment with `:=` is for more ergonomic handling of errors. `result, err := action()` is commonly repeated several times in a Go function, and it would be a burden on the programmer to give a unique name to the error variable every time.

Crystal doesn’t use error results in the same way, so I agree that we shouldn’t support reassignment with `:=`.

---

<div class="post-metadata">

**Author:** ![nobodywasishere](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nobodywasishere/32/1983_2.png) [@nobodywasishere](https://forum.crystal-lang.org/u/nobodywasishere)\
**Post date:** [January 20, 2025, 9:40pm UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/19 "2025-01-20T21:40:41Z")

</div>

From my limited testing, while `typeof(1)` and `typeof("hello")` work for type decl of instance vars (having disabled the check), trying to use `typeof(Array(String).new)` throws errors about instance vars of `Array(T)` not being explicitly typed. One of the ivars, even though it was explicitly typed, still threw an error (probably due to it being a generic type). My hypothesis is that this is due to when / how type declarations are done for ivars.

We could probably allow `:=` for literals when declaring an instance var, but doing others may require significant changes to how semantic analysis is done (or getting the type more lazily).

---

<div class="post-metadata">

**Author:** ![beta-ziliani](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/beta-ziliani/32/1821_2.png) [@beta-ziliani](https://forum.crystal-lang.org/u/beta-ziliani)\
**Post date:** [January 21, 2025, 8:49am UTC](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623/20 "2025-01-21T08:49:12Z")

</div>

My first reaction was positive. But then I got reminded on how many things are nice _per se_, but when considered as a bunch, they can be quite burdensome for newcomers.

I’m not saying we shouldn’t do it, I just think this has low priority over improving the language as is today. Getting `typeof` to work in ivars has more priority to my eyes 🙂

That said, playing around and finding out what to do in the edge cases (like multiple assignments) is a great first step.

[Next page](https://forum.crystal-lang.org/t/rfc-variable-declaration-syntax-using/7623.md?page=2)
