Skip to main content

The PM Type System


In common with other aspects of PM design, the type system is designed to facilitate the use of flexible programming constructs, such as polymorphism and dynamic dispatch, while emphasising the generation of fast, static, code.  In common with many other modern languages, PM also attempts to combine the safety of static typing with the expressivity of dynamically typed languages such as Python.
The type of any PM expression may usually be determined by a static examination of the code. Variables take their types from an initialising expression using the ‘:=’ declaration syntax popularised by Go.

 a:= 100  ! Declare ‘a’ as an integer (int) variable  

Variables do not change type during the course of their lifetime – polymorphic programming requires the use of special values, described later. Composite values such as structures are generated using specialised expressions rather than through the invocation of type-specific creator functions:

 b:= struct var_descriptor { num_values=a; var_name=”Pressure” }  

Of course, the creation of complex composite values may (and typically will) be encapsulated in a procedure:
 proc var_descriptor(n,name) = struct var_descriptor { 
   num_values=n
   var_name=name
}   

PM interprets all names starting with an underscore as being local to the module in which they are declared. Using this mechanism with structure tags and/or member names enables values to be created with an opaque type whose composition is unknown to other modules.

 proc make(x,y) = struct _my_type { x = x; _y=y }    
 ! Member x is public, _y is private to module  
All PM procedure declarations are regarded as generic templates, in the sense that separate code is generated (at least theoretically) for every combination of argument types occurring in a given program.
 proc r2(x,y)=x**2+y**2  
 a:= r2(3,4)  
 b:= r2(3.0,4.0)  
The above code will generate two different implementations of the r2 procedure, catering for integer and real arguments respectively. No runtime procedure selection (dynamic dispatch) is involved.
Of course, it will often be necessary to employ different procedure definitions for different combinations of argument types. This is achieved by applying type constraints to formal parameters.
 proc area(x: struct triangle{base,height}) = x.base*x.height/2  
 proc area(x: struct rectangle{base,height} = x.base*x.height  

The first procedure definition will only apply to structured values with the “triangle” tag and two members named “base” and “height”. The constraint does not specify types for these two members, although this could be done if required.

 proc area(x: struct triangle{base:real,height:real})= x.base*x.height/2  
Type constraints may get complicated and it is more usual to define named types (note that in PM a named type always defines a type constraint).
 type triangle is struct triangle{base,height}  
 type rectangle is struct rectangle{base,height}  
 proc area(x: triangle) = x.base*x.height/2 
 proc area(x: rectangle) = x.base*x.height  
Type definitions may also be associated with sets of type constraints, enabling type hierarchies to be created:
 type shape is triangle, rectangle  
 proc base(x:shape)=x.base  
This form of type definition creates a rigid hierarchy, but it is also possible to create open-ended types that may be expanded by later definitions, possibly in other modules.

 type shape includes triangle, rectangle  
 …  
 type polygon is struct polygon { points: point[] }  
 type shape also includes polygon  
For brevity, an “also” expressions may be combined with the type definition being added:
 type polygon in shape is struct polygon { points: point[]}  
As noted above, variables cannot change their type. In many aspects of computational programming, this will be the desired behaviour. However, some applications will require the manipulation of objects whose type is not known until runtime. In PM this is achieved using polymorphic values. A polymorphic value is a container that can hold any value conforming to a given type constraint and is created using the prefix operator <type>:
 p:= <shape>triangle(1.0,3.0)  
 p=<shape>rectangle(5.0,5.0)  
The contents of a polymorphic value are obtained using the notation *x. Since the type of *x is unknown at compile time, the correct implementation for a procedure call f(*x) may not be decidable until runtime and some form of dynamic dispatch must be used:
 shapes := <shape>triangle(0.0,0.0) dim N  
 …  
 this_shape := *shape[i] 
 this_shape_area := area(this_shape)  
The combination of polymorphic values and procedure type constraints provides a multi-methods facility, similar to that found in languages such as Julia

Comments

Popular posts from this blog

Data in, data out

When a program is running over multiple processors or processing cores then it is important to be able to map where data are flowing. One way in which data flows can be translated into programming language semantics is though argument passing to subroutines. In the absence of global variables, data transfer to and from a subroutine straightforwardly can be directly related to communication to and from another processor. In PM, the emphasis is on data parallelism through the for statement. for index in array do index=process(index) endfor      So what are the rules governing data flow into and out of this statement? These are primarily based around variable locality. x:= 1 for index in array do y:=func(index) index=process(x,y) endfor Here x is relatively global to the for statement and y is local to it (a variables scope only extends to the end of the statement block in which it is defined).  There are two ways t...

Compile time, run time, coffee time

[ Please note that in the following discussion I will be using PM 0.5 notation, which is slightly different to PM 0.4 and also in some flux as PM 0.5 is still in development. ]   Many programming languages (and most newly developed ones) include some form of compile-time programming, ranging from simple preprocessors ( #define , #if in C) to fully blown macro systems capable of re-writing the abstract syntax tree during compilation (Rust, Julia, etc .). In line with its philosophy of keeping things as simple as possible, but not simpler, PM takes a middle road with compile-time programming supported primarily through the type system. There is nothing too radical here – this is not an area where the language aims to break new ground.  The PM approach centres around the use of compile-time types. Many languages use special types to denote literal values and PM follows this trend. Literal integers, reals, strings and Booleans each have their own types: literal(int) , litera...