Understanding Primitive and Composite Types

I’m trying to understand the different types, but it isn’t completely clear.

As far as I understand, primitive types are those that contain one actual value. So are all concrete types, that aren’t containers, primitive types? Did I simplify it too much?

As for composite types, are they exclusively user defined? Or is there an existing type in Julia that qualifies as a composite type?

1 Like

almost all types are composite types. (and almost all types defined in Julia are “user” defined in the sense that they are defined in normal julia without any special compiler support).

The real difference between primitive and composite types is whether they are built out of a single region of pure bits or other types. An example of the former is Int64. it has no constituent parts, and the basic operations you can perform on it are described in the language as (essentially) specific LLVM code. However, most types are built out of other types (e.g. Rational) where operations on a Rational are written in terms of operations on the numerator and denominator.

6 Likes

To add:

The tremendous majority of Julia users (and developers) will never ever define a primitive type. There are only a handful of them in the Base language (Base.BitInteger, Base.IEEEFloat, Bool, Char, and probably something related to Ptr are the ones that come to mind) and I’ve never seen one defined in a package until I found Quadmath.jl researching this post (which adds Float128 as a primitive via library support).

If a computer doesn’t have hardware-native instructions (or, less commonly, software emulation libraries) that support the type, it probably isn’t a primitive. And Julia has already covered most of the common hardware types. The main reason I would imagine needing a new primitive type is if you were using non-IEEE-754 floats (like some of the nonstandard flavors of 16 bit floats or anything smaller) or longer IEEE floats. Or if you had some hardware or library that natively supported longer fixed-size integer types.

Another useful distinction you might be searching for is isbitstype.

5 Likes

The Quadmath case is actually quite interesting (and recent, this was only introduced ~2 months ago in cleanups by oscardssmith · Pull Request #92 · JuliaMath/Quadmath.jl · GitHub). It turns out that the Julia compiler is smart enough to pass primitive types that are AbstractFloats using floating point calling conventions rather than normal calling conventions (specifically using xmm registers on x86), so we were able to save a fair bit of complexity by just telling Julia to treat the Float128 as a bunch of bits.

4 Likes

What’s not obvious for julia users is that julia comes out of the box with very little. Almost everything you use in a julia program has been defined in the module Base in terms of simpler julia constructs. Even Float64 and Int16 aren’t built in. These types are primitive types, most of them defined in Base (a couple of them have been predefined (like Int) because they’re needed (if you write 64 it has to end up somewhere).

So, somewhere in these Base definitions there are things like,

primitive type Int32 32 end
primitive type Float64 64 end

(In julia/base/boot.jl at master · JuliaLang/julia · GitHub and julia/base/int.jl at master · JuliaLang/julia · GitHub)
You can also create your own primitive types, but there are very few opportunities to operate on them. In Base, functions like + and * on these primitive types are defined by calling “intrinsic” functions (located in Core.Intrinsics), which are not written in julia, but in C. Calling them isn’t really like calling a function, but when a julia function calling an intrinsic is compiled, the intrinsic “call” is “inlined”, i.e. it just emits machine code (or, rather, LLVM-code).

If you define your own primitive type, in order to operate on them, or even create an object, you must somehow call an intrinsic, or some C-code, or use reinterpret (which calls the intrinsic bitcast):

julia> primitive type MyInt 24 end
julia> a = reinterpret(MyInt, (UInt8(1), UInt8(2), UInt8(3)))
MyInt(0x030201)

# define a + for them:
julia> x::MyInt + y::MyInt = Core.Intrinsics.add_int(x,y)  # this will mess up you julia session
+ (generic function with 1 method)

julia> b = a + a
MyInt(0x060402)

julia> reinterpret(NTuple{3,UInt8}, b)
(0x02, 0x04, 0x06)

Similar incantations are invoked to define arithmetic and logic operations on the primitive types in Base, so that you can write 2+14.3 and similar.

Normally there’s no reason to define primitive types yourself. Some special packages do it, like BitIntegers.jl, which can be used to define e.g. 3-byte integers as above, with all the operations normally used on integers. Very similar to what Base does with Ints and Floats.

Except for these few primitive types in Base (including those you can define by @enum), everything else are composite types (Vector, Complex, Rational, IOStream, ReentrantLock etc, etc) defined in terms of other composite types, or primitive types. You can see their layout with dump:

julia> dump(IOStream)
mutable struct IOStream <: IO
  handle::Ptr{Nothing}
  ios::Vector{UInt8}
  name::String
  mark::Int64
  lock::ReentrantLock
  _dolock::Bool

A concrete type can be either primitive or composite. What distinguishes it from an abstract type is that a concrete type has a memory representation and can be instantiated as an object whereas an abstract type is merely an organizational element in the type universe, possibly with subtypes (which can be either concrete or abstract).

3 Likes

Yes, It is difficult to understand by beginners. Earlier I also have raised issue Julia Type system manual confusions.

Or neither (it is not a partition). Consider

julia> struct Foo{T}
       x::T
       end

julia> isabstracttype(Foo)
false

julia> isconcretetype(Foo)
false

julia> isconcretetype(Foo{Float64})
true

Primitive types are red herring in this discussion, and as other suggested, they should be ignored completely by new users.

Composite types are what C and a lot of other languages call structures (they even have the same name in Julia). The extra wrinkle is that they can get parameters. That’s what makes Julia Julia.

Concreteness means that all of these parameters have been specified. Eg

julia> struct Bar
       x
       end

julia> isconcretetype(Bar)
true

has no parameters, so it is concrete, even though does may not have a self-contained layout in memory.

1 Like

To expand on this, a type is concrete if it is neither the bottom type (typ == Union{}), a Union type (typ isa Union), nor a UnionAll type (typ isa UnionAll). That is, only DataType types (typ isa DataType) are concrete. (Technically, this classification is not complete, as a type may also be a TypeVar, however that is mostly an implementation detail, at least as far as beginners are concerned.)

Furthermore, subtypes of Tuple are a special case, for example: !isconcretetype(Tuple{Vararg{String}}) even though Tuple{Vararg{String}} isa DataType.

For completeness: the actual definition of concreteness is simpler than above, as in Julia’s docs, a type is said to be concrete if it is the return value of some typeof call.

See also:

1 Like