Primitive and Composite? Types


#1

What is the opposite of a primitive type? Is it a composite type?

I am reading https://docs.julialang.org/en/v1/manual/types/index.html . " Composite types are called records, structs, or objects in various languages."

Is an array composite or primitive?

(I am asking in order to accurately describe behavior when passed as argument to a function. Composite types have contents that can be assigned to, while primitive types do not?)

And what are Unions?


#2

For all practical purposes, primitives are numeric types of a fixed length, like Int64 (8 bytes), though they could theoretically be any fixed number of bytes to which a fixed meaning is assigned (Char is an example).

Composite types are types that contain any (predefined) number of other type instances. If you know any language with classes, classes are one form of a composite type (though classes do a lot more than simple data composition). Julia structs are not essential different from Julia’s tupels (except that they are distinguished by the compiler). They just give a formal name to and a way to dispatch on a certain collection of types bundled together.

Arrays in Julia are implemented in C and I haven’t looked at their implementation, but I have to assume they are composite types, since they contain information about their length and shape.

Arrays are conceptually a little different, however. In memory, they are more like a bunch of blocks of memory allocated to hold n number objects of a certain type. You can just think of them as a bunch of objects in a row, rather than one specific object unto themselves. In Julia, there is also some metadata tied to that row of objects.

Unions are a way to make something accept multiple types. Union{Int64,Float64} means that the either an Int64 or a Float64 could occur in that place. I mostly just use them to tell Julia that a type could be a specific type or nothing. (this is also a good case, since Julia can optimize for that, but a lot of other Union types are difficult to optimize.)


#3

According to the documentation of isprimitivetype, a type in Julia is primitive if it was declared using the primitive keyword. Composite types are created using either struct or mutable struct, and you can query this property using isstructtype.

Array is a bit of a special case, as it isn’t defined in Julia code but in C, so it doesn’t make sense to query whether it was 'created using struct'. However,

julia> isprimitivetype(Vector{Float64})
false

and you can see the actual implementation of isprimitivetype and isstructtype here:


#4

thanks, aaron and twan.

[1] I am trying to organize my logic now in order to explain assignments and argument passing to functions to students and myself. I think I will need to explain something like a “container-type,” where assignments create aliases and not copies. Container types encompass arrays, structs, dicts, etc. does this make sense, or do I misunderstand something? Is there already a sanctioned name?

does julia has a built-in function to learn whether a type is a container type or not? presumably, I could write a function that could work by assigning a value to an instantiated alias, and seeing whether it affects the original. ugly but functional. or does this already exist?

[2] back to Unions. I think Union{Float64,Missing} is also pretty common (not just union with nothing). I failed to figure out how to create a non-vector union type scalar instance for experimenting (otherwise, pretty useless):

julia> xv= 2.0::Union{Float64, Missing}
2.0

julia> typeof(xv)
Float64

Thus I do not know how to experiment with whether this is internally a container or a non-container type.

It is not a primitive type or an abstract type:

julia> isprimitivetype( Union{Float64, Missing} )
false

julia> isabstracttype( Union{Float64,Missing} )
false

Moreover, the docs says "A type union is a special abstract type which includes as objects all instances of any of its argument types, " and gives as an example a Union of a Float64 with Abstractstring, but this does not trigger the isabstracttype() function:

julia> isabstracttype( Union{Float64,Missing} )
false

julia> isabstracttype( Union{Float64,AbstractString} )
false

so, abstracttypes and unions are a bit confusing to me, too. is a union float-missing a primitive type or an abstract type or a … ?

/iaw


#5

First we’d need a clear definition of what a container type is.

What do you mean by assignment, exactly? Do you mean you want to test for the ability to assign a value to one of the fields of a composite type? You can do this for any mutable struct, and you can test for this property with isimmutable (which takes a value as an argument, as opposed to isprimitivetype / isstructtype).

This only asserts that the value 2.0 is either a Float64 or a Missing. Since it’s a Float64, the type assertion passes. See https://docs.julialang.org/en/v1/manual/types/#Type-Declarations-1 for more info.

It is not possible to construct a value that has a non-concrete type (test using isconcretetype). So it’s impossible to construct a value xv for which typeof(xv) returns a Union type, just like it’s impossible to construct a value of AbstractFloat type. But you can create a composite type that is able to store concrete values of either Float64 or Missing type, e.g.:

struct Foo
    xv::Union{Float64, Missing}
    # note: the `::Union{Float64, Missing}` is *not* a type assertion like before,
    # it's a specification of which values this field should be able to store
end

foo1 = Foo(missing) # works
foo2 = Foo(2.0) # works
foo3 = Foo("abc") # doesn't work

If you replace struct with mutable struct above, you’d also be able to assign values of either Float64 or Missing type to the field xv after construction.

I think either the documentation or isabstracttype should be changed. See also this TODO:

No union type is a primitive type (you can’t create a union type using the primitive keyword).


#6

No. I mean an object where assignment creates an alias (a second pointer to the same contents). If the object supports ‘.=’, it is a container type, so struct and array qualify. So does Dict:

julia> adict= Dict{String,Int64}("O" => 1, "T" => 2); bdict=adict; bdict["O"]=99; adict
Dict{String,Int64} with 2 entries:
  "T" => 2
  "O" => 99

Same for arrays. but of course for scalars,

julia> ascalar= 12; bscalar= ascalar; bscalar= 99; ascalar
12

If the scalar contents had been stored at a pointed-to destination (like an array, dict, struct, etc.), then ascalar would also have turned into 99. (Still thinking in C, I mean the difference between an object which is internally stored as a pointer to the actual content, vs an object where we store the content and not a pointer.)


Incidentally, why don’t we call it a struct rather than a composite? are there other composites than stucts?


I can not only create a struct a::Union{Float64,Missing}; end, I can also create an array of Vector{ Union{Float64,Missing}}. I now understand that I cannot create a scalar with this type. This obviates my question about whether a union is primitive or not.

now, an array seems to be a sequential bitpattern (in C), plus some header info. why does it not take more space to store a Union array if it is not simple bit-patterns? how does Julia internally store Unions?

julia> typeof( Vector{Float64}( [ 1.0, 3.0, 2.0 ] ) )
Array{Float64,1}

julia> sizeof( Vector{Float64}( [ 1.0, 3.0, 2.0 ] ) )
24

julia> typeof( Vector{Union{Float64,Missing}}( [ 1.0, 3.0, 2.0 ] ) )
Array{Union{Missing, Float64},1}

julia> sizeof( Vector{Union{Float64,Missing}}( [ 1.0, 3.0, 2.0 ] ) )
24

#7

Not so–assignment never changes any object. It just attaches a label to some existing value:

julia> a = [12]; b = a; b = [99]; a
1-element Array{Int64,1}:
 12

The fact that the label b was previously attached to the same value as a is of no consequence.

Of course, b .= [99] does mutate a as well, but that’s not assignment. Instead, it is roughly equivalent to: broadcast!(b, identity, [99]), which mutates the value to which the label b is attached. If that value is the same value to which the label a was attached, then you will see the same modification to a.

This gets to one of the nicest points about Julia (as opposed to, say, C++), which is that assignments and function arguments always work in the same way for every value. While the compiler may copy values passed into functions, it will only do so for completely immutable values, so there is no practical reason to worry about whether a copy has happened or not. Thus you can write your code without having to worry about whether a value might accidentally be copied. Likewise, there is no need to worry about whether a = b creates an “alias” or a “copy” depending on the types of a or b. All assignments behave in exactly the same way: attaching a label to a value, nothing more.


Assignment and mutation
#8

I’m also a proponent of “same thing, same name”, but I can think of two reasons for calling them composite types in the documentation:

  1. to anchor to a common term in programming languages in general
  2. the type -> mutable struct and immutable type -> struct renames happened not that long ago; maybe the documentation stems from before this rename.

See https://docs.julialang.org/en/v1/devdocs/isbitsunionarrays/#isbits-Union-Arrays-1.

I think you need Base.summarysize instead of sizeof for this purpose:

julia> Base.summarysize([1.0])
48

julia> Base.summarysize(Union{Float64, Missing}[1.0])
56

I’m actually not sure why it was decided not to include the size of the type tag array (and the header?) in sizeof(::Array).


#9

thank you, everyone. I am getting to be clearer. yes, it seems that the inconsistency is in C, not in Julia; and I have lived with C for so long that my first reaction was that C is the consistent language…

It would have been less confusing if julia had used the word ‘composite’ as the keyword, or ‘struct’ as in its docs. maybe some day…