The specific case for print can’t work, because Base.stdout can indeed be different for each Julia process:
> julia -e 'println(stderr, Base.stdout)'
Base.TTY(RawFD(17) open, 0 bytes waiting)
> julia -e 'println(stderr, Base.stdout)' > /dev/null
IOStream(<fd 16>)
Julia can’t treat something as constant (stdout) working in an environment where it is non-constant (due to IO redirection). In this specific case it probably could do so during the lifetime of the Julia process, but not beyond.
However, I think the relevant point is a different one: Printing is inherently stateful.
Handing stdout explicitly is only a small part of the problem. When you print something and the computer should output it, it needs to know where the current “cursor” is, i.e. at what position on the screen this should be printed. And to display correctly, all the previous text on the screen needs to be available, too.
You can argue that this tracking is done by the terminal, some GUI, some windows manager and/or the OS, but it does not change the fact that something needs to track this state information. And whatever method does the displaying in the end, needs to either get the state explicitly (and return the new state) or use a global variable (in general some non-pure side-effect with information from outside the method).
Random numbers are the same. A random number generator is basically a function which maps a state to the (random) number and a new state. Again: Either you handle the state explicitly and accept to carry it through your whole program via the call stack or you allow global variables in some form.
There are these inherently stateful things where most languages choose to do the implicit thing. I suggest that you read more about Haskell. Haskell was kind of designed around the idea to make all change explicit and to find abstractions to make this as painless as possible.
Putting aside that redirect_stdout needing to happen for processes outside Julia, it turns out nobody wants to do this. You yourself are trying to find a scheme for default values, so we can agree that we don’t actually want to write that extra argument (it could be a dozen in real practice). If we make the default constant (which we can do with const global variables and immutable values), then we’re forced to write more when the default value doesn’t work. We could make callable objects that store a non-default state and use that to skip writing an extra input throughout our code, but that’s still something we have to do at one point and need to pass in and out of scopes. Changing global state is just easier, and in some cases it’s justifiable.
Your proposal (for lack of a better word, because this change will never happen for Julia v1 and is unlikely to occur in any programming language) to bind values at definition time does not actually affect how defaults are handled in any significant way. Sure, there’s no relevant global variable to reassign, but a mutable value can still be mutated for the same effect. I don’t think it can be called global state because the bound value isn’t necessarily globally accessible, but external mutable state still gets you all the spooky action at a distance. The method @eval fun() = ($(Ref(0))[] += 1) has outputs that vary over the exact same (zero) inputs. That still seem predictable to you? Okay, what does for i in 1:rand(1:10) fun() end; fun() return? You really can’t do function purity halfway, you’re ending up not making a significant change at all.
rand’s default calls Random.default_rng(). The difference from reassigning a global variable is if you redefine what a function call does, you really will have to recompile all your default-generator random functions. This is done for improved performance and the much smaller need to change the random number generator. In Julia v1.12, you can reassign a const global variable (not sure if they’ll even call these variables anymore) for the same recompiling effect.
Like who? If you allow untrusted users to break your Julia process, that’s on you. If you break your own code, that’s also on you. Reassigning a global variable is the least of it, this is an interactive language, and Core.eval is easily accessible.
Let’s not use a term resembling referential transparency. Though if you want a language that doesn’t take half-measures on these concepts and does something significant, try purely functional programming languages like the previously mentioned Haskell.
I think, C++ does it for lambdas at least. There, one has to specify the default capture type, by-reference or by-copy, or do that for each captured variable individually. But that’s a statically compiled language.
Question to @Leon_Niceday : how do you propose to deal with mutually-recursive functions?
function f(x)
if x < 1
return x
else
return g(x - 1)
end
end
function g(x)
return f(x)
end
What would be the moment for function “definition”? C and C++ deal with it by requiring definitions (or at least signatures) for f(x) and g(x) in the same translation unit. But what about Julia where there is no well-defined notion of translation unit?
Lambda expressions have specific syntax with options for capture by copy or reference, not like the simple function scopes in the topic, and they can’t capture global variables at all; if you try to capture one by name, it’s ignored with a compiler warning. From the reference:
A lambda expression can use a variable without capturing it if the variable
is a non-local variable or has static or thread local storage duration (in which case the variable cannot be captured), or
Maybe I don’t understand what you mean, but I wasn’t asking to make stdout (or other global vars) constant, in for a single process or not. But instead to differently define print(x) so that it doesn’t rely on that global variable.
Could something like next be done?
print(x) = print(Core.stdout, x)
I don’t claim that this is working: it’s based on what you wrote earlier that Core.stdout is a constant denoting the real (usual) stdout, the screen.
The set of handy words is smaller than the set of possible concepts, so having meaning of words dependent on context is practically unavoidable.
I made an effort, for you and maybe others, to refine and explain the term. But since you don’t see any usefulness of the concept, seem downright against it, then why do you care how I call it?.
It could be done. You’d lose the ability to redirect stdout though.
Global variables are simply a very useful concept. An example from Base, which I am not sure anyone has brought up yet, is the configuration of the precision and rounding mode for BigFloat (julia/base/mpfr.jl at 2a5cfee2807add7babf00ddd93da505e2cce5719 · JuliaLang/julia · GitHub). Of course, you could pass the desired precision and rounding mode to every operation you perform with BigFloats, but that’s highly inconvenient.
It is not clear to me what you try to propose/suggest here. Do you completely reject global variables? Do you dislike when a function depends on other state besides that given by its arguments?
I suppose everyone here agrees that pure (in the Haskell sense) functions are simpler to reason about. They are just not convenient in all cases, and that’s when global variables are useful.
Great. Then together with print(io,x) you have all you need to print to where you want and when you want. Agreed?
How come?
I can look at your example after we clarify this.
Well… It’s a long thread, so I’ll explain. One thing is right in the title. And that would force:
I’ve been told that that cannot be done or is hard to do, or else the call syntax is more verbose. Because many functions rely on hidden global variables (as free vars in their definitions). So then I try to understand how come it can’t be done… one at a time.
No. But if you agree that they are dangerous/complicated to work with – as several people in this thread admitted – , then you should agree that language design ideas that seek to limit those global variable usage, or make it more transparent, are a good thing.
It doesn’t have to be Haskell-like. I think there can be an intermediate, still principled and useful solution.
I’m not sure that this thread is going anywhere productive:
The “why” of this is that lexical scoping is a commonplace choice that is widely adopted and widely understood across many programming languages. Yes, other choices are possible, but Julia adopting a conventional choice is not mysterious.
Julia’s basic scoping semantics are not going to change at this point: PSA: Julia is not at that stage of development anymore — you’re free to go off and implement your own programming language, but this is not the forum for it.
It doesn’t happen every day, but debating what-ifs or potential v2 behaviors isn’t off-topic. I’ve debated variable scoping, never got moved to off-topic. It might be moved to Internals and Design, but that’s not necessary either.
To prevent people jumping into the debate with unrelated concepts again, e.g. the confusion over what “transparency” means. Granted, you have a point about programming terminology depending on context, but those contexts are specified and agreed upon, not individually generated. Even disagreements warrant the respect of mutually intelligible discussion.
See how Base.stdout (but not stderr) is assigned to a different stream depending on how the Julia process is piped in the command line? Let’s put that in practice:
PS C:\#=not where you want=#> cd #=wherever you want=#
PS C:\#=wherever you want=#> julia -e 'println(""Hello world"")'
Hello world
PS C:\#=wherever you want=#> julia -e 'println(""Hello world"")' > hello.txt
(Excuse the double quotes, I’m on Windows and using Powershell.) No printout the second time, but now we have a hello.txt file in the path. If we open it, we find the text Hello world. That happened with the exact same println(x) code, so we didn’t need (or want to) refactor everything to println(io, x) inside explicit file-handling. That’s especially important for code we didn’t write, which often don’t have methods taking an extra IO argument (in fact, macro bodies can’t, only generate function calls that can). This is an expected feature of printing in particular.
I don’t think there’s any need to lock this conversation (it seems to have run its course). The basic answer to what you’re asking about is, as @stevengj said, that this is not how most (any?) mainstream programming languages work. There’s only so much “budget” for novelty in new languages, and with multiple dispatch and combining a type system with a very dynamic language, Julia already had plenty of novel things going on. Experimenting with unusual binding resolution behaviors was not something we were interested in. Julia sticks to pretty traditional—in the Lisp tradition, specifically—function and closure behavior. If someone has strong feelings that something like this could be much better done a different way, they have to have thought it through pretty well, and try it out in a new system, and convince people to try it to see if it’s actually better in practice.