« first day (1073 days earlier)      last day (1326 days later) » 

2:45 AM
hm
 
 
3 hours later…
5:37 AM
@EliahKagan haha why?
presumably the answer one would hope to get in both cases is Int32?
 
 
8 hours later…
1:12 PM
@Zanna Perhaps the post author uses Protractor.
 
@EliahKagan hehe good name
 
@Zanna Yes, I expect addition of numbers to be commutative in type as well as value.
Prohibiting heterogeneously typed arithmetic, so it causes a compile error, would also be reasonable.
 
yes
 
Since there are abstract Number, Int, and Float types anyway.
But it might be that this extremely weird behavior was the correct design choice. I am not sure.
 
in what way correct?
 
1:19 PM
Do you have icr or crystal play handy?
Actually, it's easier to test with short programs.
Do you have crystal handy? :)
 
I usually know where my protractor is, but I would like a command to locate any of the 4 or 5 pairs of scissors I have somewhere in the house
 
(I will try to answer either way, but knowing if you can easily run Crystal code at the moment might affect how I express the answer.)
So you're saying you'd like a way to find things that encompasses more than your protractor?
 
XD
 
That's an angle I had not considered.
14 messages moved from Raiders of the Lost Downboat
Hmm, aren't moved messages supposed to display with little arrows so one knows they didn't originate in the room where they appear?
I see those in AUGR but not for the messages I just moved here.
 
I see the arrows
 
1:25 PM
Interesting.
I've reloaded the page.
I see the arrow on the "14 messages moved" message but not on the actual messages that were moved.
I'm going to quit Firefox and go back in.
Then I'll say why I believe Crystal has x + y be of a different type than y + x for x and y of some numeric types. :)
 
@EliahKagan yes I have them
 
I have them now too. I see the arrows now.
 
haha that's good
lately I have to refresh the page to see the images in the userlist in chat, but maybe that's not actually a new problem (also, kind of unrelated)
 
And refreshing did not by itself make the arrows appear for me.
I'd guess it's an obscure bug that only happens some of the time and for the user who moves messages.
I don't think I've ever had it happen before.
 
I don't think I have ever seen that, but, maybe I am not enough used to the arrows that I would notice their absence
 
1:30 PM
I hope people don't conclude from these messages that one can't discuss the chat system (and its bugs) in the Downboat. But the moved messages are here, so discussion related to the may as well be here.
@Zanna Crystal is a statically typed language, so expressions (that exist in code) have types. The type of an expression is a property of the code itself; it exists statically and is known to the compiler. (In contrast, in a dynamically typed language, things that exist at runtime--values or objects--have types, but while a program may have expressions that happen always to evaluate to something of the same type, the type is not really a property of the expression.)
 
I think we discussed this in some detail before
 
Yes.
 
or rather, you explained it in some detail, and I, as I recall, more-or-less got the idea eventually
 
A variable, by itself, is an expression, and this distinction between statically and dynamically typed languages is true of variables in particular as well as expressions in general. People often characterize the difference between statically and dynamically typed languages in terms of variables specifically, but I think this is often misleading, especially when type inference comes into play.
Almost every statically typed language infers the types of compound expressions like x + y from the types of their subexpressions. Some statically typed languages infer the types of variables as well. For example, in Crystal, if the only time you assign to the variable x is by writing x = 3 and you do not otherwise tell the compiler what type x is, then x is of type Int32 because 3 is of type Int32.
To be precise--more precise than is usually necessary but exactly as precise as is necessary in discussing local variables' types in Crystal--it is actually occurrences of expressions that have types in a statically typed language.
 
so the type of an expression might vary... like, x might not be Int32 if it ends up having some other values besides 3, that are of different types?
 
1:42 PM
Yes. But that is a way in which Crystal is unusual. It is not unusual, though, for separate occurrences of the same expression (or separate expressions that consist of the same sequence of tokens, if you consider separate occurrences of the same expression to really be separate expressions: it's a matter of definition) to have different types.
So, for example, in this program, x + y has one type in one place and another type in another place, and it is not at all unexpected, and every statically typed language allows something like this:
def add_integers(x : Int32, y : Int32)
  x + y
end

def add_floats(x : Float64, y : Float64)
  x + y
end

p add_integers(1, 2)
p add_floats(0.1, 0.2)
The reason is that the x parameter in add_integers and the x parameter in add_floats aren't even the same variable. They're in separate scopes. They just happen to have the same name.
Because Crystal supports overloading, add_integers and add_floats could have the same name. They would still be different methods.
def add_(x : Int32, y : Int32)
  x + y
end

def add(x : Float64, y : Float64)
  x + y
end

p add(1, 2)
p add(0.1, 0.2)
Because Crystal supports genericity (and even defaults to it), you could also remove the type annotations on the parameters, and write it generically. It might seem like there is one add method that takes different types at runtime, but instead what's happening is that the compiler is instantiating separate add methods from the generic definition.
In one of them, x and y are both Int32, and in the other, x an y are both Float64. (Because those are the values I called it with and the compiler instantiates generic methods based on how they are called.)
def add(x, y)
  x + y
end

p add(1, 2)
p add(0.1, 0.2)
The typeof operator gives the static type of an expression. This program:
def add(x, y)
  puts "x : #{typeof(x)}"
  puts "y : #{typeof(y)}"
  x + y
end

p add(1, 2)
p add(0.1, 0.2)
Prints:
x : Int32
y : Int32
3
x : Float64
y : Float64
0.30000000000000004
Another way to reason about the types of generic-method parameters is to think of them as being type parameters (though I don't know if that term is considered the technically correct one for Crystal). For example, you could think of that program as being this equivalent program and (with that program in mind) say the types of x and y are T and U respectively:
def add(x : T, y : U) forall T, U
  x + y
end

p add(1, 2)
p add(0.1, 0.2)
You can also write and run that code. You do not have to use the names T and U so it is not, of course, really true of the earlier program that didn't name the types to say that the types of x and y were T and U.
Having named the types, you can use the names:
def add(x : T, y : U) forall T, U
  puts "x : #{T}"
  puts "y : #{U}"
  x + y
end

p add(1, 2)
p add(0.1, 0.2)
As you'd expect, that produces that output as well.
In the way add is called (and thus in the way the compiler instantiates it to produce concrete methods that work for specific types and from which actual assembly language code can be emitted), the type of x is always the same as the type of y. Specifically, as I've called add, the arguments passed for those parameters are either both Int32 or both Float64. But as written, add can be called with arguments of different types.
I could prevent this by constraining them to be the same type:
def add(x : T, y : T) forall T
  x + y
end

p add(1, 2)
p add(0.1, 0.2)
All of this is normal, in the sense that you get it in all or almost all other languages that have constructs for programming in these styles. That is, in any language with a static type system, you can have expressions that are the same sequence of characters but have different types, and you can have separate variables of the same name in different scopes.
In languages that support overloading, you can have multiple, separately implemented functions or methods of the same name. These create separate scopes in the same way that separate methods or functions of different names do.
In languages that support generic programming, you can have have a generically coded method or function that is used to instantiate/generate multiple actual function implementations.
What is less common, however, is that, in Crystal, the types of separate occurrences of the same variable can be different. Many languages don't allow this at all. Even those that do don't usually have it happen to the same broad extent as it can in Crystal.
For example, this program:
x = 5
p typeof(x)
x = "hello"
p typeof(x)
Prints:
Int32
String
This is the sort of thing you'd expect to see in a dynamically typed language, where objects or values have types but expressions (and variables) do not. But Crystal is a statically typed language. It's not merely that x happens to hold a value that is an Int32 after the first assignment and happens to hold a value that is a String after the second assignment.
The compiler knows what kind of thing it holds, and makes the variable's actual static type different in different places accordingly.
 
2:14 PM
@EliahKagan yay I remembered what "overloading" is
 
@EliahKagan yay this follows from what you were saying
 
@EliahKagan You might ask why Crystal allows this. After all, it seems like the programmer should have used two separate variables there anyway.
 
@EliahKagan I guess so
 
The reason is that the more complicated cases are useful. I presume the less useful simple case is permitted at least in part because prohibiting it would make the rules more complicated.
Sry, was afk.
 
2:29 PM
me too :)
 
The type of a variable in Crystal -- that is, of an occurrence of a variable -- is the union of all the types it can be based on assignments that may happened to it.
Consider this program:
if ARGV.empty?
  x = 5
else
  x = ARGV[0]
end

p typeof(x)
Its output is:
(Int32 | String)
That is the case regardless of whether any arguments are passed to the program. After all, Crystal is statically typed, and the type of an expression is a property of the program, not a property of the data that exist at runtime during a particular run of the program.
(Int32 | String), which can also be written without the parentheses as Int32 | String, is a union type: a type that can take on any Int32 value or any String value.
If you think of types as being like classes (in the mathematical sense) or sets (in the mathematical sense) then the word "union" for this makes sense. :)
Now consider this program:
if ARGV.empty?
  x = 5
  p typeof(x)
else
  x = ARGV[0]
  p typeof(x)
end

p typeof(x)
When run with an argument, that program prints:
String
(Int32 | String)
When run with no arguments, it prints:
Int32
(Int32 | String)
What's happening here is that typeof(x) in the if branch is always Int32, and typeof(x) in the else branch is always String, but which branch runs depends on behavior at runtime.
 
@EliahKagan oh! that is illuminating. It has these types before it has any values
 
2:45 PM
Yes. Crystal is statically typed. So the types are a property of the program (which may differ in different places in the program). The values are a property of a particular run of the program (which may differ at different times).
You can sometimes infer by analyzing a program's source code that some expression always has some particular value, but the value of a variable is a runtime property, whereas its type is a compile-time property (or, really, a property of the source code itself, whether or not you compile it).
 
@EliahKagan why is typeof(x) in the else branch always String? Does that mean that the argument will have that type whatever it is?
 
What other type could it have?
 
I don't know, but, not everything is a string?
 
I very much agree that not everything is a string. :)
Are you thinking, for example, that if you pass something like 42 as the argument, then ARGV[0] should have one of the numeric types, such as Int32?
 
yes
 
2:51 PM
Command-line arguments are always strings. A program can then parse them to (attempt to) produce a value of another type.
 
I see :)
 
In particular, in Crystal the type of ARGV is Array(String).
 
hmm
why is it Array?!
 
Because it's an array.
ARGV[0] is the first command-line argument, ARGV[1] is the second, and so forth.
 
oh! of course!
 
2:54 PM
Like @ in a Bourne-style shell.
Except that positional parameters in Bourne-style shells are indexed starting from 1 instead of starting from 0.
 
but @ is all the arguments, isn't it? Each one being put into 1, 2...
so @ is an array
well that had not occurred to me...
 
@ is effectively an array in Bourne-style shells, yes. Even those that don't support arrays.
 
:) :)
 
Technically I suppose it is not ever an array in Bourne-style shells, but rather is just a special shell parameter for which exceptions are made to the quoting rules so that stuff like "$@" can expand to multiple (or zero) words.
Btw, you can use this for writing a portable shell script that only needs one array, by rewriting @ using set.
In effect, all Bourne-style shells have one array (per scope). Some offer additional arrays as an extension. :)
Regarding using command-line arguments as numbers, consider this program:
n = ARGV[0].to_i
puts "#{n} squared is #{n**2}."
The to_i method on String as Int32 as its return type. Since ARGV has type Array(String) and 0 has one of the integer types (it has type Int32 but you can use others for indexing if you want), ARGV[0] has type String, so ARGV[0].to_ihas type Int32.
n has type Int32 regardless of whether or not any command-line arguments are passed or what their values are.
After all, the type of n is a property of the program, known to the compiler. What happens when you actually run the program... well, that might be fifty years later! :)
This relates to the strategy to_i uses when it fails.
It throws an exception instead of returning. You can catch the exception. If you don't, the program crashes with a helpful (to the programmer) message. However, here, it didn't even get that far:
Unhandled exception: Index out of bounds (IndexError)
  from ../../../../usr/share/crystal/src/indexable.cr:589:8 in '[]'
  from arg.cr:1:5 in '__crystal_main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:105:5 in 'main_user_code'
  from ../../../../usr/share/crystal/src/crystal/main.cr:91:7 in 'main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:114:3 in 'main'
  from __libc_start_main
  from _start
  from ???
[Sorry, apparently the crystal command only treats -- specially when there is at least one argument after it. :( I've fixed the example. This is all the more reason to use crystal build and then run one's executable. :)]
There, evaluating the expression ARGV[0] failed. Evaluation occurs at runtime, so the failure occurred at runtime. When I say the evaluation failed, I just mean the operation that was attempted did not succeed.
It threw an exception, so the [] method never returned.
I didn't write code to catch the exception, so the program crashed with that message.
@EliahKagan Actually, -- is not supported at all. This is strange, because I thought I'd used that before with crystal... and also I thought I'd read that it worked, either in the language reference or the book. Hmm.
@EliahKagan Unfortunately my edit made that incomprehensible because I didn't show the command. Here:
$ crystal arg.cr
Unhandled exception: Index out of bounds (IndexError)
  from ../../../../usr/share/crystal/src/indexable.cr:589:8 in '[]'
  from arg.cr:1:5 in '__crystal_main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:105:5 in 'main_user_code'
  from ../../../../usr/share/crystal/src/crystal/main.cr:91:7 in 'main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:114:3 in 'main'
  from __libc_start_main
  from _start
  from ???
@EliahKagan If I instead pass an argument, but it's not convertible to an Int32, then I get:
$ crystal arg.cr hello
Unhandled exception: Invalid Int32: hello (ArgumentError)
  from ../../../../usr/share/crystal/src/string.cr:424:5 in 'to_i32'
  from ../../../../usr/share/crystal/src/string.cr:325:5 in 'to_i'
  from arg.cr:1:1 in '__crystal_main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:105:5 in 'main_user_code'
  from ../../../../usr/share/crystal/src/crystal/main.cr:91:7 in 'main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:114:3 in 'main'
  from __libc_start_main
(If I'd had crystal saved the compiled executable by compiling it with crystal build arg.cr then I would run it in the usual way, with that becoming ./arg and that becoming ./arg hello.)
And of course if I run it with something that can be converted to an Int32 then it works:
$ crystal arg.cr 17
17 squared is 289.
I can catch the exception:
begin
  n = ARGV[0].to_i
  puts "#{n} squared is #{n**2}."
rescue ArgumentError
  STDERR.puts %[#{PROGRAM_NAME}: error: can't square "#{ARGV[0]}"]
  exit 1
end
Then it works the same when the input is good:
$ crystal arg.cr 17
17 squared is 289.
But:
$ crystal arg.cr hello
/home/ek/.cache/crystal/crystal-run-arg.tmp: error: can't square "hello"
Note that this program, as written, still crashes when no arguments are passed (and does something other than what the user probably intended when multiple arguments are passed, as it only uses the first one but does not warn that there were others).
In case you're wondering how to express $0 in Crystal, it's PROGRAM_NAME.
When the expression ARGV[0].to_i is evaluated, the to_i method never returns. It throws an exception of type ArgumentError instead. The exception object contains more information about the problem, but I didn't use that information. My rescue clause only catches ArgumentErrors, so for example if I don't pass any arguments, the exception thrown in the [] method is an IndexError and I don't catch that:
$ crystal arg.cr
Unhandled exception: Index out of bounds (IndexError)
  from ../../../../usr/share/crystal/src/indexable.cr:589:8 in '[]'
  from arg.cr:2:7 in '__crystal_main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:105:5 in 'main_user_code'
  from ../../../../usr/share/crystal/src/crystal/main.cr:91:7 in 'main'
  from ../../../../usr/share/crystal/src/crystal/main.cr:114:3 in 'main'
  from __libc_start_main
  from _start
  from ???
This is good because my my exception handler (the code in the rescue clause) would be wrong for that.
The ArgumentError exception is thrown (or "raised," as Crystal and some other languages call it) from code in the implementation of String#to_i. But there is no handler for that exception in that scope, so the exception propagates up the chain of method calls (note: what that chain actually is is a runtime property, as the same method may be called from more than one place and may or may not be called at all depending on decisions made at runtime).
It keeps moving up, until it gets to a rescue clause that can catch it. If there isn't one, then the program crashes an prints a stack trace.
I can catch both exceptions separately:
def die(message)
  STDERR.puts "#{PROGRAM_NAME}: error: #{message}"
  exit 1
end

begin
  n = ARGV[0].to_i
  puts "#{n} squared is #{n**2}."
rescue IndexError
  die "too few arguments"
rescue ArgumentError
  die %[can't square "#{ARGV[0]}"]
end
The String type, in addition to a to_i method with return type Int32, also has a to_i? method with return type Int32 | Nil. This type can also be written as Int32?. The Nil type has just one value, nil, and it represents what is conceptually the absence of a value. String#to_i? returns an integer when the string is convertible and nil when it is not.
So, starting again with the simple program that you might say has two crash bugs:
n = ARGV[0].to_i
puts "#{n} squared is #{n**2}."
The behavior of that version can alternatively be achieved by using to_i? and checking for nil:
n = ARGV[0].to_i?

if n.nil?
  STDERR.puts %[#{PROGRAM_NAME}: error: can't square #{ARGV[0]}]
  exit 1
end

puts "#{n} squared is #{n**2}."
If you comment out the exit 1 line, the program will not compile:
Showing last frame. Use --error-trace for full trace.

In arg2.cr:8:26

 8 | puts "#{n} squared is #{n**2}."
                              ^-
Error: undefined method '**' for Nil (compile-time type is (Int32 | Nil))
What's happening is that the exit method has the special return type NoReturn which means it can never return. Whenever n is nil, the code under if n.nil? runs, and no code after it ever runs because exit never returns. So the type of n is Int32 | Nil everywhere in the code from where n is introduced up to an including the if-condition n.nil?, but then in the if clause the type of n is Nil, and after the if clause the type of n is Int32.
 
4:09 PM
Likewise, arrays have a []?method as well as [], and the behavior of that version, which handles both possible runtime errors can alternatively be achieved by using []? and to_i?:
def die(message)
  STDERR.puts "#{PROGRAM_NAME}: error: #{message}"
  exit 1
end

arg = ARGV[0]?
die "too few arguments" if arg.nil?
n = arg.to_i?
die %[can't square #{ARGV[0]}] if n.nil?
puts "#{n} squared is #{n**2}."
Since exit has return type NoReturn and every code path (in this case, the one and only code path) through my die method calls exit, my die function also has return type NoReturn.
Just after arg = ARGV[0]?, the variable arg has type String | Nil.
But then after die "too few arguments" if arg.nil?, the arg variable has type String. (In the code guarded by arg.nil?, which consists of die "too few arguments", the arg variable would have type Nil.)
Likewise, and as in the previous example, just after n = arg.to_i?, the variable n has type Int32 | Nil.
But then after die %[can't square #{ARGV[0]}] if n.nil?, the variable n has type Int32. (In the code guarded by n.nil?, which consists of die %[can't square #{ARGV[0]}], the n variable would have type Nil.)
To verify this, consider this modified program that prints the static types:
def die(message)
  STDERR.puts "#{PROGRAM_NAME}: error: #{message}"
  exit 1
end

arg = ARGV[0]?
puts "arg : #{typeof(arg)}"
die "too few arguments" if arg.nil?
puts "arg : #{typeof(arg)}"
n = arg.to_i?
puts "n : #{typeof(n)}"
die %[can't square #{ARGV[0]}] if n.nil?
puts "n : #{typeof(n)}"
puts "#{n} squared is #{n**2}."
If I run that, and pass 17 as the argument, I get:
arg : (String | Nil)
arg : String
n : (Int32 | Nil)
n : Int32
17 squared is 289.
Btw, another example of a method in Crystal's standard library that has a return type of String? (i.e., of String | Nil) is gets.
print "Enter a number to square: "
line = gets

if line.nil?
  puts "\nGot end-of-input."
  exit
end

n = line.to_i?
if n.nil?
  puts "I can't square that."
else
  puts "That squares to #{n**2}."
end
Just after line = gets, the variable line has type String | Nil. In the code guarded by line.nil?, it has type Nil. In that code, exit, which has return type NoReturn, is always called, so control ever leaves that code. So afterwards, line has type String.
Just after n = line.to_i?, the variable n has type Int32 | Nil. But in the if clause guarded by n.nil?, it has type Nil, and in the else clause, it has type Int32.
(Btw, I don't know if I am using the correct terminology in describing code guarded by an if, an else, a while, etc., as a "clause." The clause might be just the if or else (or whatever) and where applicable it condition. Code guarded by such a construct is, in the jargon associated with some languages, called a "block" or a "suite." The most natural and widespread term is "block," but I'm avoiding it because, like in Ruby, in Crystal a block is a very specific thing.)
If I put code after that if...else in which n appeared, its type would again be Int32 | Nil.
So anyway, in those examples, a variable has a broader type earlier in the code and a narrower (i.e., more restrictive) type in later code that can only sometimes be reached. But the opposite can also occur. Consider this program:
answer = 42
p typeof(answer)
answer = ARGV[0] unless ARGV.empty?
p typeof(answer)
Whether or not you pass a command-line argument, that prints:
Int32
(Int32 | String)
Consider also this program:
answer = 42
p typeof(answer)
answer = ARGV[0] unless ARGV.empty?
p typeof(answer)
answer = 1.234 if answer.is_a?(Int32)
p typeof(answer)
That program always prints:
Int32
(Int32 | String)
(Float64 | String)
At the end of the code, the type is Float64 | String rather than Int32 | Float64 | String because anytime the value is an Int32 it gets replaced by a Float64 value.
 
5:00 PM
I should say that the real reason is that there are specific rules for how the type changes across the code amidst if, unless, while, until and other things involving boolean, .nil?, is_a?, responds_to?, and other things. You can write a program for which you can prove that it would be safe for a variable to be considered of a more restrictive type than Crystal infers it to be. Actually it's quite easy. For example, the output of the program
x = 10
x = "hello" if false
p typeof(x)
is
(Int32 | String)
even though the value held by x is always 10.
It would be easy to cover very simple cases like that, but there is no good reason for Crystal to be designed that way, because basically the only cases where one writes code that way is to produce branching artificially in order to simulate a real runtime test. Also, although it would be easy to cover it, it's not at all obvious how it should be covered. It's clear that, after the code that never runs, the type could safely still be Int32.
But what should the type of the variable inside the code that never runs?
Consider:
x = 10

if false
  p typeof(x)
  x = "hello"
  p typeof(x)
end

p typeof(x)
We don't see it because that code never runs (and provably so), but in the if false clause, the first typeof(x) evaluates to Int32 and the second one, after the assignment, evaluates to String.
There is a fact of the matter as to what those expressions evaluate to because, unlike most operators, typeof is evaluated at compile-time rather than at runtime.
But you might wonder how I can actually know what it evaluates to, and also why it matters. The answer is that you can see types of things in dead code (code that can never run because program logic never gets to it) in error messages from the compiler.
Consider this modified program, which does not compile:
x = 10

if false
  x = "hello"
  p x + 1
end

p typeof(x)
The compile error is:
Showing last frame. Use --error-trace for full trace.

In dead.cr:5:7

 5 | p x + 1
         ^
Error: no overload matches 'String#+' with type Int32

Overloads are:
 - String#+(other : self)
 - String#+(char : Char)
+ is used for adding numbers and for concatenating text, but you cannot + text with a number, which is what that error is saying.
But if x kept type Int32 at the end of the program (which would be safe, since false can never be true so the code that assigns a String to x can never run), then what should the type of x be inside the code that can never run?
Logically, it seems like it should be something like a union of all possible types. After all, the code can never run, so anything goes!
(Alternatively, a design decision could've been made to simply make it a compile error to have code that can so easily be proved dead.)
So anyway, it is useful to be able to conditionally assign to a variable, thereby causing its type--in subsequent occurrences in the program--to become the union of its old type and its possible new type.
Furthermore, it is useful to be able to conditionally assign an expression to a variable that contains that variable as a subexpression.
First, consider this program, in which the type is guaranteed to stay the same:
n = ARGV[0].to_i
n += 1 if ENV.has_key?("INCREMENT")
p n
$ crystal increment.cr 42
42
$ INCREMENT= crystal increment.cr 42
43
n += 1 is short for n = n + 1.
In Crystal, Tthe type of nafter n += 1 will always be the same as after n = n + 1.
There is actually a difference between expr1 += expr2 and expr1 = expr1 + expr2, in that the former can have fewer occurrences of a subexpression which is thus evaluated fewer times--which is usually desired. For example, with an array a and a method f that accepts an argument x and returns an integer, the compound assignment a[f(x)] += 1 evaluates f(x) once and thus calls f once, while the simple assignment a[f(x)] = a[f(x)] + 1 evaluates f(x) twice and thus calls f twice.
But they have exactly the same behavior in many uses (and nicely, it's the ones where one intuitively thinks they do).
So the type of n += 1 is the same as the type of n = n + 1, which is the same as the type of n + 1 (and also that is the type of the value assigned to n and the type n has after the assignment).
But increasing a variable often--and in particular in the case of increasing it by 1--feels like an operation that does not change a type.
Suppose, instead of the initial assignment to n having been:
n = ARGV[0].to_i
That it was:
n = ARGV[0].to_u64
Then n would have type UInt64 instead of Int32.
In that case, what should the type of n + 1 be, bearing in mind that 1 has type Int32 (because it is an integer literal that fits in the range of Int32 and it has no suffix such as i64 or u64 to make it be of some other type) and that n += 1 has the same type as (and causes the type of n to be the same type as) that of n + 1?
 
5:33 PM
sorry! I'll catch up tomorrow, some weird voting stuff going on this week I got pulled into discussion about
 
Intuitively, if n starts out as an integer type other than Int32--I'll continue using UInt64 as the example--then the type of n after n += 1 may or may not be evaluated should still be UInt64 (and its value should just be the next value), rather than the type becoming Int32 | UInt64. I believe it is to make that happen that the type of the expression x + y, where x and y are of two different integer types or two different floating point types, is the type of x.
In terms of how it is implemented, I think it also happens to be that it is somewhat easier to do it that way (and I believe I know why that is the case too), but I don't think that's the fundamental reason.
@Zanna No problem! I hope everything is okay!
 
:) all good thanks
 
Although to most programmers experienced with other general-purpose programming languages the business with the same local variable taking on arbitrary different types in different parts of the program text within its scope is probably what's least intuitive (it took me a bit to understand it), I think the part of what I said that might be most confusing may actually be the small bit about exception handling. You could skim that and go back to it if so.
Exception handling is an interesting and important topic, but I didn't cover it in much detail, nor did I undertake much effort to make it intuitive, because it was secondary to the business with unions and type inference that I was on about.
 

« first day (1073 days earlier)      last day (1326 days later) »