Strings & Numbers

Before taking a stroll in the diverse garden of Par’s types, let’s stop by the most basic ones: the primitives.

At the moment, Par has four primitive types:

  • Int — Integers, positive and negative whole numbers, arbitrary size.
  • Nat — Natural numbers, starting from zero, arbitrary size. They are a subtype of Int.
  • String — UTF-8 encoded sequencee of Unicode characters.
  • Char — Singular Unicode characters.

There’s a significant distinction between primitives and all other types in Par.

The thing is, Par has a fully structural type system. All custom type definitions are just aliases — there is no way to create opaque types. (But, encapsulation is perfectly possible.)

Primitives are different in that they are opaque. They are magical types, distinct from others, that are operated on using magical built-in functions. This is necessary to achieve their efficient representation.

Primitives are manipulated using magical built-in functions.

To find the list of all built-in functions:

  1. Open the playground.
    $ par-lang playground
    
  2. Press Compile, and Run. Scroll the list that pops up. List of built-in functions

To figure out the type of a built-in function:

  1. Assigning it to your own def, such as:
    def Examine = Int.ToString
    
    Par knows the type of Int.String, so it will infer it for Examine as well.
  2. Press Compile.
  3. Move the cursor the the definition. The playground will display the type on the right, in green. Displayed type of a built-in function

The type [Int] String is a function from Int to String. We will cover functions and other types in detail later. Despite that, we’ll still play with some built-in functions in this section. All you need to know is that the square brackets enclose function arguments, and the result type follows. For example:

  • [Int, Int] Int is a function from two Ints to an Int.
  • [Int, Nat, Nat] Nat is a function from one Int and two Nats to a Nat.

The current set of built-in functions is very minimal. They’re just enough to be able to write more useful functions yourself, but they’re nowhere close to a standard library. For example, there are no functions for analyzing strings, aside from String.Reader, which is flexible enough to implement all you’d need. You just need to do it yourself.

Keep in mind that Par is early in development. It’s bringing an innovative paradigm, which we’re still figuring out how to use best. Creating an expansive standard library would be premature before we understand what’s actually going on here.

Now, let’s take a look at the primitives!

Int

Integers are arbitrarily sized whole numbers, positive or negative.

Their literals consist of digits, optionally prefixed with - or +, and may include underscores for readability.

def Num1: Int = 7
def Num2: Int = -123_456_789

The type annotations are not needed:

def Num3 = 42
def Num4 = -2202

Without annotations, Num3 actually gets inferred as Nat. But, since Nat is a subtype of Int, it can be treated as an Int too.

Built-in functions are used for arithmetic operations. For example:

def Num5 = Int.Add(3, 4)  // = 7
def Num6 = Int.Mul(3, 4)  // = 12

Go ahead and explore more of them in the playground!

Nat

Natural numbers are just integers excluding the negative ones. Nat is a subtype of Int, so every variable of type Nat can be used as an Int, too.

def Num7 = 14  // inferred as `Nat`
def Num8 = 17  // inferred as `Nat`

// perfectly valid
def Num9 = Int.Add(Num7, Num8)

While Num7 and Num8 are inferred as Nats, Num9 will be an Int because that’s what Int.Add returns. To get a Nat result, use Nat.Add, which only accepts Nats:

def Num10 = Nat.Add(Num7, Num8)  // inferred as `Nat`

Several built-in functions aid in converting Ints to Nats. For example:

  • Nat.Max has type [Nat, Int] Nat — the second argument is allowed to be an Int. Yet it’s guaranteed to return a Nat.
  • Int.Abs has type [Int] Nat — an absolute value is always a Nat.
def Num11: Nat = Nat.Max(0, -1000)  // = 0
def Num12: Nat = Int.Abs(-1000)     // = 1000

Unlike Ints, natural numbers can be looped on using Nat.Repeat, which is one of their main uses. We’ll learn more about that in the section on recursive types.

String

Strings are represented as UTF-8 encoded sequences of Unicode characters. Their literals are enclosed in double quotes ("), and may contain escape sequences, such as \n, familiar from other languages.

def Str1 = "Hello"  // inferred as `String`
def Str2 = "World"

To concatenate strings, use String.Builder. To fully understand how it works, we’ll need to cover iterative and choice types, but perhaps you can get the idea:

def Str3 = String.Builder
  .add(Str1)
  .add(", ")
  .add(Str2)
  .build  // = "Hello, World"

Analyzing strings — such as finding, splitting, or parsing — is done using String.Reader. To be able to use it, more knowledge of the language is needed first. But, feel free to play with it in the playground, or check out the StringManipulation.par example in the examples/ folder.

Numbers can be converted to strings using Int.ToString:

def Str4 = Int.ToString(14)  // = "14"
def Str5 = Int.ToString(-7)  // = "-7"

Note, that Nat is a subtype of Int, so any natural number can also be converted to a string this way, too. In fact, that’s exactly what happens with Str4.

Char

A Char is a single Unicode character. Char literals are enclosed in single quotes:

def Char1 = 'a'  // inferred as `Char`
def Char2 = '\n'

There’s a built-in function to check if a Char is a part of a character class:

def IsWhitespace = Char.Is(' ', .whitespace!)  // = .true!

There’s no built-in function turning a String to a list of Chars. Feel free to copy-paste this one, if you ever need it:

dec Chars : [String] List<Char>
def Chars = [s] String.Reader(s).begin.char.case {
  .end! => .end!,
  .char(c) rest => .item(c) rest.loop,
}