Types Intro
Shift plays around with a few differents of type theory. There are 3 major ones to understand.
- Values
- Types
- Kinds
This section assumes the reader understands some basics such as what is a variable and the difference between a string and an integer. This is not a good place to learn programming if you cannot code. That said it will walk through what typing is incase you never really learned it and/or cargoculted through the concepts in your career.
Overview
I am in no way an expert on type theory, so the explaination above is how I've wrapped my head around the concept, not a PhD's mathematically explaination. I am sure there are gaps and easy holes a mathmatician could poke at this. Never the less; my lack of nomeclature and proper prose around type theory does not make the concepts being conveyed invalid, just unprecise compared to academia.
At the bedrock of the language we have the axiom data structures. Building blocks that all other structures in the language are built out of. These are sometimes called the primitives, built-in, seed, or standard types. For Shift anything that can be repersented on its own as a literal, as the primitive types of the system.
As a programmer in virtually all languages there are a few you're most likely familiar with.
- Numbers
- Flags
- Text
- Byte
I intentionally used a more human friendly name just to demostrate that virtually all langauges have these. Sometimes the language is very sparse and gives you just byte with it's 0 to 255 possible values othertimes it's nice and presents a good foundational set of values to work with. All data is repersented by this and/or combination of bytes.
Shift's supports all of the primitives that C# does. Below are the most foundation ones
- bool/bit: a value containing 0 to 1
- Array: a fixed length collection of something
- byte: an array of 8 bits giving 0 to 255 values
- Which triggers the chicken egg question of some tautological definitions.
- int
- char
- string
The primitive building blocks are bootstrapped by the language. We may be able to express them in terms of the langauge or other building blocks, but they are axiomnotic. We aren't going to get into bootstrapping computing and computer science bedroom. I promise to move higher level to something more applicable and useful from here out, but the context is important.
Those closer to some of the more subtle parts of C# may understand that the above primitives are sometimes value types and sometime reference types. There is no such distinction in Shift. So if you're aware of it in C#, throw it out mentally, and if you aren't we'll come back to how it behaves.
Values
In Shift the first type universe is a Value.
If you aren't familiar with the term type universe then mentally replace it with type tier. Tier of defining a data's structure/form, how information is formed in the language.
Some common values
- 3848547 - an int
- "Why howdy partner" - a string
- false - a boolean
See Literals for an exhaustive list of primitives
These values are what we would call constants. The value is 100% definable at compile time and is immutable. It is constant it's in definition and what it would evaluate to.
A value can be assigned to a variable. In math more generically we may call this a term.
var foo = 32;
It's called a variable because it's a term whose value may vary. By that, we mean that the variable will store/hold/reference/point/contain different values at differnet points in time; however it's easiest to think of, and this isn't low level so the distinction isn't important.
The value itself never changes. The number 32 is ALWAYS 32 but the term foo may have a different value at later points in time.
foo = 7;//Now it contains the value 7
foo = 0;//Now it contains the value 0
foo = 32;//Now it's back to it's original value of 32
var bar = "honk";
bar = "quack";
bar = "oink";Foo varies over time, or at least has the potential to. Hence why we call it a variable. At least why I call it one :)
7, 0, and 32 share an interesting property and so do "honk", "quack", "oink". These sets belong to different types. The first being an Int32 and the second being a String.
Types
A type can be thought of as a template for a value. It's the definition, constraints, contract of configurations or possibilities that value will have. A non-exhausitive list of some primitive types
- Boolean will be either true or false.
- Char will be a 16 bit unicode character ranging from U+0000 to U+FFFF
- Int32 is -2,147,483,648 to 2,147,483,647
- Uint64 is 0 to 18,446,744,073,709,551,615
- String is a fixed length Array of Char
Blue is the name of the type and yellow is the range of values it can be. This range of values is the contract. Int32 guarentees is never be larger than 2,147,483,647. Also String helps demostrate that a Type's definition can be a combination of other Types. All of other types are too, if we really get into the bedrock but not a discussion for here.
Because everything is derived from binary, a type is a contract of what combination of 0s and 1s will appear. Since a type can be defined as a combination of with other types, we use these abstractions to build up to more meaningful repersentations of our work.
For a type if you mentally imagine enumerating, loop through, all possible combinations you end up with a collection of values that match that type. For Byte this would be 0-255. Normally we think of this in the other direction. For a value, what collection does it belong to? That is the type of the value.
Take the value 0
Is this a bit/flag/boolean, a byte, an integer? If it's an integer what size 16, 32, 64 bit? Is it signed or unsigned? Is it floating point, double, or decimal? Is it ascii character 0 or a string of text that is just 1 character 0?
In Shift, like C#, each primitive type has a different way of coding it's value in source code. This helps remove ambiguity. 0 is an Int32. See the Primitives page for an exhaustive list.
var foo = 0;
Since 0 is an Int32, that means variable foo's type is the same as the value's it's being assigned's type. From here on, we know that foo will only ever contain a value that is within the collection of possible values that the Int32 type allows.
var foo2 = 3f;
This statement would supply foo2 with a floating point number 3 (float which aliases Single).
var foo3 = "3";
Finally this statement would supply foo3 with a string containing a single char 3. Just to help demostrate similiar looking things, something that we may even say outloud identically in english; It's just 3, have different types in Shift's type system.
Type Inference
When declaring a variable for use, you have to specify the type that the variable will be. The syntax for this is as follows
{type} {identifier}
So far all of the examples above have used var as their type in this syntax. This is not a type. It's a placeholder for a type that the compiler will supply. You can imagine it as
WhateverTypeTheCompilerDeterminesIsCorrect foo = 7;
The compiler determines what to supply by looking at the incoming value on the right side of the = operator. Whatever type this value is, is the value that compiler uses for var. In our above case since the value is 7, it's type is an Int32. That mean's foo's type will be Int32.
A programmer can explicitly write this as
Int32 foo = 7;
The reason most variables are typed with var instead of their explicit type is it is redundant. Since the compiler can determine the variable's type via the supplied value, it does not need to be supplied on both sides. This act of determining the type via the supplied value is called Type Interference. The static type is inferred from other parts of the statement.
Type inference can only be used when the type is deterministic and unambiguious. Here are two common examples where the intended type cannot be inferred.
var foo;
When declaring a variable and taking the default value instead of using an initializer the type must be specified. This is because there is nothing on the right side of the = to infer against. You must instead write int foo; to be able to declare an integer variable without an initializer.
Second example:
The initializer here is ambiguious on what type the returned value will be. It will be either a string or an Int32. In this case you would need to specify that foo is a type union, pick a common base, or refactor your code so that there is not two types.
Example of what a type union in this situation would look like as a possible solution.
See Type Unions for more information
Type Safety
Shift is both statically and strongly typed. What this means is
- Static - The type of something cannot be changed once created.
- Strong - Operations between things will enforce constraints related to types.
Shift provides Type Safety at both compile and runtime. It is a major philosophical goal of the language that as much as possible occurs at compile time, so you get fast feedback on errors.
The primary goal of Type Safety is to ensure you do not attempt to change the type of something or break a constraint. Below is a trivial example
var foo = 32;//foo is an int (Int32) via type inference
foo = 27;//It can be assigned a new int value at any type
foo = "honk";//The compiler throws an error on this lineThe compiler throws an error on the 3rd line because foo is of type int and the assign valued is a string.
Type Safety will ensure that any interactions of types and values match their destination target. This is useful for ensuring the wrong thing isn't assigned to a variable or passed into a method. As a developer you can rely on the types being whatever they're declared to be.
Complex Types
In constract to the primitive types, whose values can be coded as literals, there are complex types. These are types that are can only be expressed and programmed as a combination of other types. These types can be both primitives or other complex types. At the end of the day all complex types ultimately terminate in primitive types with literal repersentations.
Complex types have no literal repersentation. To create assign a value of a complex type you need to create an instance. Let's look at a simple complex type; heh oximoronic.
record PersonData
{
string Name;
int Age;
//Constructor
this(bool isAdult)
{
Age = 21;
}
//Another constructor
this(string name, isAdult) : (isAdult)
{
Name = name;
}
}
var a = new PersonData();
Console.WriteLine(a);//PersonData
Console.WriteLine(a.Name);//""
Console.WriteLine(a.Age);//0The new keyword allows us create an instance of a type. This creating an instance, instantiating, a type produces a value of that type. Drawing attention back to literals. The compiler supplies bootstrapping the instantiation.
When using the new keyword you supply it the desired Type and constructor overload.
Note how Name and Age have no value assigned to them. They use their own type's default constructor. For string (String) this is "" (empty string) and for int (Int32) this is 0.
Constructors
Constructors are special methods that are first run or initializer blocks of code for setting up/configurating complex types. They are defined by a method with no return type and a name of this. You'll also notice that inside of the method there is no return statement. Both the return type and returned value from the method are inferred and system supplied.
You imagine the system supplied method under the hood looking something like this.
Repersentative
Record types are immutable and can only be `in flux` inside of their constructor. Once the method has been initialized it's locked in whatever state it is. So you would not be able to pass a PersonData into a method and then modify it; in this way the above way is repersentative. On the flip side, it is bootstrapped magic that allows a constructor to modify a record's values inside of the constructor. There is no syntax that morphs the record from modifiable to unmodiable or back. This would be an interesting way of making the language more complete mathematically.
Every type has a system supplied default constructor that takes no arguments. A user can define extra operators with any number of operators they want, as shown back above in the record's definition with the 2 different operators.
var a = new PersonData(); This uses the () constructor instance, which is not defined on in PersonData. This is uses that system supplied constructor as mentioned. A user can also supply their own default constructor implementation. Let's update our record's definition.
record PersonData
{
string Name;
int Age;
this()
{
Name = "Joe Smith";
Age = 54;
}
}
var a = new PersonData();
Console.WriteLine(a);//PersonData
Console.WriteLine(a.Name);//"Joe Smith"
Console.WriteLine(a.Age);//54You can assign initial values to fields directly to help reduce code too.
record PersonData
{
string Name = "Joe Smith";
int Age = 54;
}
var a = new PersonData();
Console.WriteLine(a);//PersonData
Console.WriteLine(a.Name);//"Joe Smith"
Console.WriteLine(a.Age);//54Finally remember what we learned from Type Inference the reason that variable a is a value of type PersonData is known via type inference. The new command returns a value of whatever type it is given. The compiler inspects this when inferring the type for var.
We mentioned a few situations where one may be required to specify the type explicitly instead of use var. There are also times as a programmer you may prefer to use an explicit type over type inference purely out of convenience; even if the compiler could correctly infer a type.
The primary case for this is when your variable will be taking the default value for a type.
//Instead of
var a = new PersonData();
//A cleaner and terser
PersonData a;For those coming from many object oriented languages it's important to draw attention to the differences in behavior for the second statement. There is no such concept as null or undefined in Shift. Everything has a fully initialized default value. If no initializer is provided, a system supplied new Type(); is supplied.
Finally there is something called initializer syntax, where the fields of a type can be assigned inline alongside the new statement.
var a = new PersonData
{
Age = 28,
Name = "HONK BOB 5000"
}When using initializer syntax you cannot supply a constructor invocation. Hence the lack of () anywhere. Furthermore the fields and their assignments can be any order, it is not critical that they are in the same order as they appear in the Type.
Complex Types Continued
As mentioned a complex type can contain another complex type. Let's expand our example.
record PersonData
{
string Name = "Joe Smith";
int Age = 54;
}
record MoreComplex
{
PersonData Boy;
PersonData Girl;
this((string boy, string girl) names, int age)
{
Boy = new PersonData { Name = names.boy, Age = age };
Girl = new PersonData { Name = names.girl, Age = age };
}
}
var foo = new MoreComplex
{
Boy = new PersonData { Name = "Jack", Age = 8 },
Girl = new PersonData { Name = "Jill", Age = 9 }
}
var bar = new MoreComplex(("Sid", "Sydney", 15));Showing off use of nested complex types with both initializer and constructor invocation.
Kinds
The rules governing what can be placed into a type and the mutability vs immutability after it's creation are governed by the type of type it is. This collection of types is known as Kinds. There are a few system supplied kinds in Shift.
- Record - Immutabile datastructures that can only be comprised of other records.
- All primitives are records
- Structure - Mutable datastructures that can be comprised of both structures and records.
- Library - A singleton collection of named methods with shared immutable state.
- Unlike the other kinds, a library produces a value and not a type.
- Service - Dedicated to business logic. It's fields are immutable but can contain any other kind of type. You can think of the fields are bound scope for the instance of the service.
- Enum - A named list of values
- Technically this is liked an instance of an immutable Dictionary<Symbol,T>
- Interface - A contract that record, structures, and services can implement.
At this time there are no user defined kinds.
Type TypeArray = T:Array<S:int,Type>;
//var parameterArguments = [int, bool, string];
//Inferred as Array<3, Type> {int, bool, string }
//TypeArray ta = parameterArguments;//Since it's just an alias to this type
structure Tuple<TA:TypeArray>
{
this(Tuple<TA> values)
{
//In practice the tuple type is seeded
//If you think about it, you must pass in a tuple of values to a tuple to start it
//That passed in tuple's type signature much matter the new instance of the tuple
//In this way tuples, are elementary like Arrays.
//We can express their behavior and signature within the langauge
}
}
type ParameterSet = T:Tuple<TA:TypeArray>;
//Really complicated way of saying it's an instantied instance of a tuple
//With these specific values in this order
//For example
//var tuple = new Tuple<[int,bool,string]>(3, false, "honk");
//var tuple = new Tuple<ta>(3, false, "honk");