Julia – 35 – tipi – 4

Tipi composti mutabili If a composite type is declared with mutable struct instead of struct, then instances of it can be modified:

In order to support mutation, such objects are generally allocated on the heap, and have stable memory addresses. A mutable object is like a little container that might hold different values over time, and so can only be reliably identified with its address. In contrast, an instance of an immutable type is associated with specific field values –- the field values alone tell you everything about the object. In deciding whether to make a type mutable, ask whether two instances with the same field values would be considered identical, or if they might need to change independently over time. If they would be considered identical, the type should probably be immutable.

To recap, two essential properties define immutability in Julia:

An object with an immutable type is passed around (both in assignment statements and in function calls) by copying, whereas a mutable type is passed around by reference.

It is not permitted to modify the fields of a composite immutable type.

It is instructive, particularly for readers whose background is C/C++, to consider why these two properties go hand in hand. If they were separated, i.e., if the fields of objects passed around by copying could be modified, then it would become more difficult to reason about certain instances of generic code. For example, suppose x is a function argument of an abstract type, and suppose that the function changes a field: x.isprocessed = true. Depending on whether x is passed by copying or by reference, this statement may or may not alter the actual argument in the calling routine. Julia sidesteps the possibility of creating functions with unknown effects in this scenario by forbidding modification of fields of objects passed around by copying.

Tpi dichiarati The three kinds of types discussed in the previous three sections are actually all closely related. They share the same key properties:

They are explicitly declared.

They have names.

They have explicitly declared supertypes.

They may have parameters.

Because of these shared properties, these types are internally represented as instances of the same concept, DataType, which is the type of any of these types:

A DataType may be abstract or concrete. If it is concrete, it has a specified size, storage layout, and (optionally) field names. Thus a bits type is a DataType with nonzero size, but no field names. A composite type is a DataType that has field names or is empty (zero size).

Every concrete value in the system is an instance of some DataType.

Tipi unione A type union is a special abstract type which includes as objects all instances of any of its argument types, constructed using the special Union function:

Tipi parametrici An important and powerful feature of Julia’s type system is that it is parametric: types can take parameters, so that type declarations actually introduce a whole family of new types – one for each possible combination of parameter values. There are many languages that support some version ofgeneric programming, wherein data structures and algorithms to manipulate them may be specified without specifying the exact types involved. For example, some form of generic programming exists in ML, Haskell, Ada, Eiffel, C++, Java, C#, F#, and Scala, just to name a few. Some of these languages support true parametric polymorphism (e.g. ML, Haskell, Scala), while others support ad-hoc, template-based styles of generic programming (e.g. C++, Java). With so many different varieties of generic programming and parametric types in various languages, we won’t even attempt to compare Julia’s parametric types to other languages, but will instead focus on explaining Julia’s system in its own right. We will note, however, that because Julia is a dynamically typed language and doesn’t need to make all type decisions at compile time, many traditional difficulties encountered in static parametric type systems can be relatively easily handled.

All declared types (the DataType variety) can be parameterized, with the same syntax in each case. We will discuss them in the following order: first, parametric composite types, then parametric abstract types, and finally parametric bits types.

This declaration defines a new parametric type, Point{T}, holding two “coordinates” of type T. What, one may ask, is T? Well, that’s precisely the point of parametric types: it can be any type at all (or a value of any bits type, actually, although here it’s clearly used as a type). Point{Float64} is a concrete type equivalent to the type defined by replacing T in the definition of Point with Float64. Thus, this single declaration actually declares an unlimited number of types: Point{Float64}, Point{AbstractString}, Point{Int64}, etc. Each of these is now a usable concrete type:

The type Point{Float64} is a point whose coordinates are 64-bit floating-point values, while the type Point{AbstractString} is a “point” whose “coordinates” are string objects.

Point itself is also a valid type object, containing all instances Point{Float64}, Point{AbstractString}, etc. as subtypes:

Other types, of course, are not subtypes of it:

Concrete Point types with different values of T are never subtypes of each other:

Warning: This last point is very important: even though Float64 <: Real we DO NOT have Point{Float64} <: Point{Real}.

In other words, in the parlance of type theory, Julia’s type parameters are invariant, rather than beingcovariant (or even contravariant). This is for practical reasons: while any instance of Point{Float64} may conceptually be like an instance of Point{Real} as well, the two types have different representations in memory:

An instance of Point{Float64} can be represented compactly and efficiently as an immediate pair of 64-bit values;

An instance of Point{Real} must be able to hold any pair of instances of Real. Since objects that are instances of Real can be of arbitrary size and structure, in practice an instance of Point{Real} must be represented as a pair of pointers to individually allocated Real objects.

The efficiency gained by being able to store Point{Float64} objects with immediate values is magnified enormously in the case of arrays: an Array{Float64} can be stored as a contiguous memory block of 64-bit floating-point values, whereas an Array{Real} must be an array of pointers to individually allocated Real objects – which may well beboxed64-bit floating-point values, but also might be arbitrarily large, complex objects, which are declared to be implementations of the Real abstract type.

Since Point{Float64} is not a subtype of Point{Real}, the following method can’t be applied to arguments of type Point{Float64}:

A correct way to define a method that accepts all arguments of type Point{T} where T is a subtype of Real is:

Equivalently, one could define function norm{T<:Real}(p::Point{T}) or function norm(p::Point{T} where T<:Real); see UnionAll Types [prossimamente].

More examples will be discussed later in Methods.

How does one construct a Point object? It is possible to define custom constructors for composite types, which will be discussed in detail in Constructors [prossimamente], but in the absence of any special constructor declarations, there are two default ways of creating new composite objects, one in which the type parameters are explicitly given and the other in which they are implied by the arguments to the object constructor.

Since the type Point{Float64} is a concrete type equivalent to Point declared with Float64 in place of T, it can be applied as a constructor accordingly:

For the default constructor, exactly one argument must be supplied for each field:

Only one default constructor is generated for parametric types, since overriding it is not possible. This constructor accepts any arguments and converts them to the field types.

In many cases, it is redundant to provide the type of Point object one wants to construct, since the types of arguments to the constructor call already implicitly provide type information. For that reason, you can also apply Point itself as a constructor, provided that the implied value of the parameter type T is unambiguous:

In the case of Point, the type of T is unambiguously implied if and only if the two arguments to Point have the same type. When this isn’t the case, the constructor will fail with a MethodError:

Constructor methods to appropriately handle such mixed cases can be defined, but that will not be discussed until later on in Constructors [prossimamente].