3 hours later…
8 hours later…
1:19 PM
1:25 PM
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.)
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
.
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.)
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:
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
.
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)
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.
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.
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 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.
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.
2:45 PM
2:51 PM
2:54 PM
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. :)
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_i
has 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! :)
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.
@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 ???
$ 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
.)
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
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).
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 ArgumentError
s, 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.
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.
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}."
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
.
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
.)
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}."
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.)
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:
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
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
.
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.
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.
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.
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
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
.
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.
« first day (1073 days earlier) ← previous day next day → last day (1326 days later) »