Types In Julia

static types where every program expression must have a type computable before the execution of the program.

dynamic types where nothing is known about types until run time, when the actual values manipulated by the program are available.

  • when types are omitted they are `Any` type

  • adding annotations serves three primary purposes

    • to take advantage of Juli'a powerful multiple dispatch
      • to improve human readability
        • to catch programmer errors

Julia's type system is dynamic, nominative, and parametric

  • generic types can be parameterized
  • hierarchial relationships btwn types are explicitly declared
    • rather than implied by compatible structure
  • concrete types may not subtype each other
    • abstract types can be their supertype

All nodes are equally first-class as types, that belongs to a single, fully connected type graph

There is no compile time type, the only time a value has is its actual type when the program is running, this is called run-time type where the combination of static compilation w/ polymorphism makes this distinction significant.

variables are bound to values, values have types

abstract and concrete types can be parameterized by other types

  • this can be parameterized by values of any type for which `isbits` returns true

Abstract Types

Abstract types cannot be instantiated, and serve only as nodes in the type graph

abstract type <<name>> end
abstract type <<name>> <: <<supertype>> end

the `abstract type` keyword introduces a new abstract type, whose name is given

  • the name is optionally followed `<:` and an already existing type, indicating that the newly declared abstract type is a subtype of this `parent` type
    • the default supertype is `Any`
  • Julia has a predefined abstract "bottom" type, at the nadir of the type graph, the opposite of the Any is `Union{}`
    • no object is an instance of Union{}
      • and all types are supertypes of `Union{}`

Primitive Types

""" it is always preferable to wrap an existing primitive type in a new composite type than to define your own primitive type.

this functionality exists to allow Julia to bootstrap the standard primitive types that LLVM supports. Once they are defined, there is very little reason to define more. """

A Primitive type is a concrete type whose data consists of plain old bits.

  • Julia lets you declare your own primitive types

    primitive type <<name>> <<bits>> end
    primitive type <<name>> <: <<supertype>> <<bits>> end
    
  • the number of bits indicates the storage the type requires and the name gives the new type a name

    • A primitive type can optionally be declared to be a subtype of some supertype
      • supertype defaults to `Any`
  • Only sizes that are multiples of 8 bits are supported

    • you will experience LLVM bugs with sizes other than that
    • booleans cannot be smaller than 8 bits

Composite Types

Composite types are called records, structs, or objects in various languages

  • A composite type is a collection of named fields
    • an instance of which can be treated as a single value
      • most commonly used user-defined type in Julia

        "in C++ and Python composite types also have named functions associated with them, and the combination is called an object. In SmallTalk, all values are objects whether they are composites or not. In C++ and Java, some values, such as integers and floating-point values, are not objects, while instances of user-defined composite types are true objects with associated methods."

        In Julia, all values are objects, but functions are not bundled with the objects they operate on

        • this is necessary since Julia chooses which method of a function to use by multiple dispatch
          • meaning that the types of all of a function's arguments are considerd when selecting a method, rather than just the first one
            • it would be inappropiate for functions to belong only to their first argument
              • organizing methods into function objects
                • rather than having named bags of methods "inside" each object
                  • ends up being a highly beneficial aspect of the language design
struct Foo
    bar
    baz::Int
    qux::Float64
end
foo = Foo("Hello, world.", 23, 1.5)
typeof(foo) # Foo
fieldnames(Foo)
# (:bar, :baz, :qux)
foo.bar # "Hello, world"

when a type is applied like a function, two constructors are generated automatically

  • these are default constructors
    • One accepts `Any` arguments can calls `convert` to convert them to the types of the fields
    • The other accepts arguments that match the field types exactly

Composite objects declared with struct are immutable; they cannot be modified after construction

  • some structs can be packed efficiently into arrays

    • compiler can avoid allocating immutable objects entirely
  • it is not possible to violate invariants provided by the type's constructor

  • code using immutable objects can be easier to reason about

    • immutable object might contain mutable objects
      • contained objects will remain mutable
      • only the fields of the immutable object itself cannot be changed to point to different objects

if all fields of an immutable struct are indistinguishable `===` then 2 immutable values containing those fields are also indistinguishable

struct X
    a::Int
    b::Float64
end

X(1, 2) === X(1, 2)
true

For many user-defined types X, you may want to define a method Base.broadcastable(x::X) = Ref(x) so that instances of that type act as 0-dimensional "scalars" for broadcasting

Parametric Types

Types can take parameters, so that type declarations actually introduce a whole family of new types - one for each possible combination of parameter values

Generic Programming in ML, Haskell, C++, Java, C#, Scala

  • ML Haskell Scala support true parametric polymorphism
    • others support ad-hoc, template-based styles like C++ and Java

      Julia is dynamically-typed language and doesn't need to make all type decisions at compile time

      • All declared types `DataType` can be parameterized
        • parametric composite types

          struct Point{T}
              x::T
              Y::T
          end
          
          Point{Float64} <: Point{Int64}
          # false
          Float64 <: Real
          # true
          Point{Float64} <: Point{Int64}
          # false
          
        • parametric abstract types

          abstract type Pointy{T} end
          Pointy{Int64} <: Pointy
          # true
           Pointy{Real} <: Pointy{Float64}
          # false
           Pointy{Float64} <: Pointy{<:Real}
          # true
          struct Point{T} <: Pointy{T}
              x::T
              y::T
          end
          
          Point{Float64} <: Pointy{Float64}
          Point{Float64} <: Pointy{<:Real}
          
          abstract type Pointy{T<:Real} end
          
          struct Point{T<:Real} <: Pointy{T}
              x::T
              y::T
          end
          # true
          
        • parametric primitive types