Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What to do with global variables? #3139

Closed
asterite opened this issue Aug 11, 2016 · 33 comments
Closed

What to do with global variables? #3139

asterite opened this issue Aug 11, 2016 · 33 comments

Comments

@asterite
Copy link
Member

Today we had a big discussion in Manas about what to do with global variables.

This will be long, but it's necessary to understand the problem.

First, let's talk about class variables. What does the following program print, to you?

puts Foo.var

class Foo
  @@var = 1

  def self.var
    @@var
  end
end

You probably guessed one of these:

  1. It gives an error saying "undefined constant Foo"
  2. It prints nil
  3. It gives an error, saying that @@var must be Int32, not Int32 | Nil
  4. It prints 1

The current behaviour is the last one: it prints 1.

The reason for this is that it's very hard (maybe impossible) to do these two things at the same time:

  1. Have a first compiler pass that declares types and the type of instance, class and global variables, and a later pass to type "main" code (so we "hoist" type definitions, like Javascript does, and this is different than Ruby)
  2. Have the compiler verify that one of those variables is not used before the line where it was defined

An example to understand why this is difficult is this one:

class Parent
  def foo
    1
  end
end

class Holder
  def initialize(@parent : Parent)
  end

  def foo
    @parent.foo
  end
end

Holder.new(Parent.new).foo # problematic line

class Child < Parent
  @@const = 1

  def foo
    puts @@const
  end
end

A first compiler pass declares all types, methods, and the type and initializers of class and instance variables.

A later pass types the "main" program, which in this case is Holder.new(Parent.new).foo. When the compiler types this it sees that @parent.foo is invoked, and @parent is of type Parent or any of its subclasses, so both Parent#foo and Child#foo are typed. When typing Child#foo we see that @@const is used, but wait, this happened before we had a chance to initialize @@const = 1... so should an error be given? We did that in the past, and it brought a lot of false positive cases, like the above one. No, @@const isn't really used before it was defined in the above example.

However, if we replace "main" with this:

Holder.new(Child.new).foo

Now yes, it's used before it was defined. The problem is that the compiler can't distinguish both cases: it will always type @parent.foo with @parent being Parent or any of its subclasses.

One possible solution, and the one we are currently doing, is to lazily initialize the class variable @@foo. The line @@foo = 1 is actually instructing the compiler to generate an initializer for the class variable. This initializer is lazily executed: either the first time when it's accessed, or when the compiler reaches the line @@foo = 1 when generating code (when traversing the syntax tree).

This brings us back to why the original example printed 1. In a way, we think this is not that bad: @@foo = 1 really looks like a class variable declaration, and so using it in a few lines above is just using the "initializer" value. As a side note, I think it's very uncommon to find such code, and one wouldn't write it on purpose, but it frees one from having to be strict about declaration order, and forward declarations are never needed.

(This is actually true for constants, they work the same way (the difference is that constants can't be reassigned))

Again, trying to detect if a class variable or constant is used before it was declared, while doing hoisting at the same time, is very complex, and in fact I can't find an existing (compiled) language that does that. So the current behaviour is mostly correct and intuitive, and frees the programmer from a lot of problems.

Now, on to global variables. The problem with them is that there's no syntax to declare them. You just have $x = 1, which can either be the first time they are assigned (thus declared) or an update to them. With class variables it's more obvious: either the assignment happens at the class level, so it's a declaration, or it happens inside a method, so it's an update (yes, one can update a class variable at the class level, but I can't see a use case for that, and it's probably not an important case to consider).

I don't want to extend this more, so we concluded we have three choices:

1. Remove global variables

We remove global variables from the language. You can use class variables or constants as a replacement. The variables $~ and $? stay, as they aren't really global (they are method-local).

2. Keep global variables, but introduce syntax to declare them

It will be:

def $x = 0

They will work very similar to class variables: they have to have a declaration, and it happens lazily, or they can omit the declaration in which case the type is inferred from assignments to them, but they will be nilable.

3. Keep global variables, consider the first assignment to them as the initializer

So this:

puts $x
$x = begin
  puts "Hello"
  1
end
puts $x

will print "Hello", then 1, then 1 again (the initializer is run either the first time the global variable is used).

Choice

My personal option for this is to remove global variables. In my opinion their usefulness is limited and they can always be replaced with class variables and constants. Adding special syntax just for global variables doesn't seem right. But of course we'd really like to know your opinion! :-)

(Also see #1395, though I wouldn't personally introduce more special $... variables)

@jhass
Copy link
Member

jhass commented Aug 11, 2016

I'm all in for removing them. We probably should add class_property and related macros at the same time though.

@refi64
Copy link
Contributor

refi64 commented Aug 11, 2016

I personally favor 2. Yeah, globals are evil, but:

  • If Toyota overuses them, it's their problem.
  • They can make it extremely convenient when prototyping.

@drosehn
Copy link

drosehn commented Aug 12, 2016

I wish I had more experience of programming in crystal before trying to compare the choices. After the little bit of crystal programming that I've done so far, I'm a little surprised at how often I resort to global variables in the early stages of writing a program.

To what extent are "global" variables tied to modules? Would it help (at all) to say that user-defined $-variables are tied to a module, such that the programmer has to define a module to use them? And then to say that $-variables are something like @@class-variables, but at a module level?

@drosehn
Copy link

drosehn commented Aug 12, 2016

( After thinking about it some more, I realize this was a dumb question. Please ignore! )

@drosehn
Copy link

drosehn commented Aug 12, 2016

And then to continue with the idea of global-variables being tied to modules, you could then say that global variables would be defined after the module Whatever line, the same way that class-variables are defined after the class Whatever line:

module Curses
   #   <- global variables would be defined here ->

  class Window
  end
end

@asterite
Copy link
Member Author

@drosehn I meant to say that this is uncommon:

puts Foo.var # <--- this, here, before even declaring Foo

class Foo
  @@var = 1

  def self.var
    @@var
  end
end

and in fact in Ruby there's no way to do that.

Could you show some code that you've written where you used global variables? I'd like to see some real use for them, other than for short scripts.

Then, global variables tied to a module are what class variables are. That's why we are proposing removing global variables in favor of class variables.

Finally, searching for "Ruby global variables" gives some very interesting results, with some quotes I will include:

I mean, if we can have a language where a thing that's strongly discouraged in the language from which it was inspired, I think that's a good idea, right? We try to do that with everything we want to improve over Ruby.

@drosehn
Copy link

drosehn commented Aug 12, 2016

I'm sorry to say that I'm unprepared for a good discussion of this, and yet I do think it's a very significant issue and thus I think it deserves a good discussion. My gut feeling is that it would be nice to do away with truly global variables, but I do seem to use global variables more than I realized.

When I'm writing a program, I'm usually starting out under some time-pressure and with no time to sketch out the whole program before I start. That isn't the way I want to work, but it's the nature of the job I have. Due to that, I often find myself with classes which weren't designed quite right, and thus I need global variables to do the very thing that I should not be doing. Which is to say, I use them to get some rarely-changing information from one class to some other class.

So for that situation, it's convenient to have variables that are not tied to a specific class, but they don't have to be truly global. Can I use @@-variables which are defined in a module, but outside of any specific class? My first attempt to do that did not work out quite right, but it is pretty likely that I'm just doing something wrong.

@drosehn
Copy link

drosehn commented Aug 12, 2016

In some other discussion about this topic I mentioned that I frequently take advantage of global variables when I start writing some new program, because I don't really know what I'm doing yet. But once I have a better understanding of what I need to write, then those global variables disappear, and are replaced by class-variables or other methods.

And do to that, the programs where I am using global variables are almost certainly ones which I wouldn't want to show to anyone else. I already know the program is a disorganized mess, so there's nothing there I would want to show off! :)

@drosehn
Copy link

drosehn commented Aug 12, 2016

And while I'm admitting my ignorance, I'll also note that I don't know what @jhass means by

adding class_property and related macros.

What features would those macros provide?

@jhass
Copy link
Member

jhass commented Aug 12, 2016

module MyGlobals
  class_property  kill_all_humans = false
  class_setter    answer : Int32
  class_getter!   question : String
  class_property? alive : Bool = true
end

would be the same as

module MyGlobals
  @@kill_all_humans = false

  def self.kill_all_humans
    @@kill_all_humans
  end

  def self.kill_all_humans=(value)
    @@kill_all_humans = value
  end

  @@answer : Int32

  def self.answer=(value : Int32)
    @@answer = value
  end

  @@question : String?

  def self.question
    @@question.not_nil!
  end

  def self.question?
    @@question
  end

  @@alive : Bool? = true

  def self.alive?
    @@alive
  end

  def self.alive=(value : Bool)
    @@alive = value
  end
end

@sdogruyol
Copy link
Member

I'm all in for removing global variables 👍

I know it's not a direct comparison but for example Java also doesn't have global variables.

@asterite
Copy link
Member Author

@sdogruyol lol, I said exactly the same thing yesterday to @waj

@kofno
Copy link

kofno commented Aug 12, 2016

I think you can safely get rid of global variables. It's a fairly small shift to move from using a global reference to a class level reference.

@macoca
Copy link
Contributor

macoca commented Aug 12, 2016

👍 for removing global variables!

@drosehn
Copy link

drosehn commented Aug 12, 2016

@jhass : Thanks. Now that I see what you mean, it seems very obvious! Yes, I'd like to see those.

Now that I've had more time to think about it (and to get some sleep!), I think it's best 👍 to drop global variables for crystal programs. I would list all my thoughts on this, but I think I've already written more than enough for one topic. :)

@drosehn
Copy link

drosehn commented Aug 12, 2016

Last night I had trouble getting a @@class-variable to work at a module level, but I started with a fresh new program today and they're working fine for me. I must have been doing something wrong last night, and was too tired to see it.

@Sija
Copy link
Contributor

Sija commented Aug 12, 2016

I was going to favor solution #2, but after some thought on the subject, I'm inclined to vote for removal. There are available rather convenient ways to go without them, like constants, class/module properties or singleton pattern to name the few, so removing them wouldn't leave programmer without other sensible options to replace 'em.

@RX14
Copy link
Contributor

RX14 commented Aug 12, 2016

I'm very much in favour of option 1, creating a singleton to run your program in, or using a module for constants is an easy enough workaround for hacky scripts, and has benefits.

@matiasgarciaisaia
Copy link
Member

I think the easiest tendency is to say "globals are evil - burn'em to death!", and most people would agree in a discussion.

I think this issue should be all about making a good case for them. Not every language have globals, but a great amount of them do. That's not a reason to say "hey, they have'em, we should too", but it'd be useful to understand why it is that they do support globals.

Instead of "yay/nay", I'd like this issue to be more about "this language/framework uses globals for this use case - but Crystal could make blah instead".

@jhass
Copy link
Member

jhass commented Aug 12, 2016

Please note that this is about removing $globals, not the possibility to have global state.

@ozra
Copy link
Contributor

ozra commented Aug 13, 2016

I definitely, definitely, vote for removing globals. Classvars are so much better for global state.
As for the rest mentioned, Crystal does just as it should imo.

@f
Copy link
Contributor

f commented Aug 13, 2016

I don't see any reason to have global variables. And they cause diseased programs. Let's remove them.

@luislavena
Copy link
Contributor

Hello,

In my personal opinion, I'm in favor of complete removal of global variables, including the special cases $~ and $?.

Bear with me for a minute: the special cases $~ (regular expression matching) and $? (process exit state) are two elements that might be problematic to keep up as they are now and potentially with threading.

They require special conditions (by using thread-storage to ensure these values do not interfere similar usage on other threads).

Why not drop them entirely? If someone wants to capture the MatchData of Regex, simply use #match and done.

If someone wants to capture the status of a process, simply use Process.run.

By entirely remove references to $whatever, it removes these Perl-isms (or Rubysm, whatever are you coming from) and keep things consistent and simplifies Crystal codebase at the same time.

On all the years that I have used Ruby, only once I have relied on $? after using backticks to run a process and was simply to capture the output on a quick script. The more complex the script and the subprocess got, I was forced to drop the naive approach (unbuffered, non-streaming) and properly use pipes and redirection, thus ending there the life of my $? usage.

But again, this is just my personal opinion.

If you reached this point, thank you for your time.

Cheers!

@jhass
Copy link
Member

jhass commented Aug 13, 2016

$? and $~ are already thread local by being stack local, they're set in the parent stack frame directly.

@luislavena
Copy link
Contributor

@jhass correct, but they require special code to handle that. Keep the code to handle only these two special cases seems overly complicated (again, my personal opinion).

@jhass
Copy link
Member

jhass commented Aug 13, 2016

Well, see #1395 for my opinion on that ;)

@ghost
Copy link

ghost commented Aug 14, 2016

I vote remove globals

Coming from Java to Ruby to Node.js to Elixir to Node.js again and now trying out Crystal. (Yes the concept of Node.js is cool, but I miss the Ruby syntax). The mutable global shared state is a huge problem almost everywhere. Elixir uses immutable data; it can only be re-bound. All consumers who accessed the value before it was rebound won't see the new value. No locking, No leaking; problem solved.

@sumproxy
Copy link

sumproxy commented Aug 14, 2016

I agree on removing global variables, too

@wmoxam
Copy link
Contributor

wmoxam commented Aug 15, 2016

Remove globals! 💯 🌠

@idchlife
Copy link

Guys, I'm sorry but how then to pass a variable or constant to scope of method/function?
I have multiple variables outside method body and have undefined local variable error

@mverzilli
Copy link

@idchlife I think the best place to ask this question is in Google Groups: https://groups.google.com/forum/#!forum/crystal-lang

When you do, please provide a code snippet to help us understand what you mean and the actual use case you're trying to tackle so we don't end up with an http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem

:)

@idchlife
Copy link

@mverzilli Thank you! I tried stack, but it was ghosttown with crystal-lang

@mverzilli
Copy link

Yeah, for a more chat based experience head to https://gitter.im/crystal-lang/crystal or Crystal's IRC (they're bridged, so it's basically the same): https://crystal-lang.org/community/#gitterirc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests