Skip to content

Trait

Trait is a feature that allows you to reduce code duplication.

Why do we need trait?

Suppose we have two type of structure, which is People and Fruit,

1
2
3
4
5
6
7
def People
    :name    String
    :age     Number

def Fruit
    :name    String
    :size    Number

Then, suppose we have a list of People and a list of Fruit, and we wish to find out the maximum of each list.

  • For the People list, the maximum means the People with the largest age.

  • For the Fruit list, the maximum means the Fruit with the largest size.

To achieve that, we created two functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Function for searching the max People in a People list
def (this List{People}).max -> People
    let mutable result = this.(0)
    for i in 1.to(this.length - 1)
        if this.(i):age > result:age
            result = this.(i)
    return result


// Function for searching the max Fruit in a Fruit list
def (this List{Fruit}).max -> Fruit
    let mutable result = this.(0)
    for i in 1.to(this.length - 1)
        if this.(i):size > result:size
            result = this.(i)
    return result

If you've notice carefully enough, the two functions only differs by one line, which is line 5 and line 14 (as highlighted in the code snippet above).

This indicates code duplication, which is bad and dangerous.


In general, there are two ways for solving the code duplicaiton problem:

  1. Using lambdas (a.k.a first-class functions)

  2. Using trait

Using lambda

To understand why trait is necessary, let us first look at how lambda can solve the problem, as shown below.

1
2
3
4
5
6
7
// Declare the max function which takes a lambda/function
def (this List{T}).maxBy(comparer Function{Tuple{T, T} to Boolean}) -> T
    let mutable result = this.(0)
    for i in 1.to(this.length - 1) 
        if comparer.invoke((this.(i), result))
            result = this.(i)
    return result

To use the function above,

1
2
// suppose `peoples` is a variable that is declared
let maxPeople = peoples.maxBy(_:age > __:age) 

So, what's the problem with using lambdas?

  1. Only people that understand lambdas understand the hackish looking code

  2. Code maintainability is reduced (due to 1.)

  3. Reusing the same function requires more typing (implying minor code duplication)

Note

Despite the disadvantages described above, there are one major benefit of using lambdas, which is flexibility. So, in a situation where flexibility is more favorable, you should use lambdas instead of traits.


Using trait

In general, we need 4 steps to use trait:

  1. Define a trait with some name

    • Usually, the name should be an adjective which ends with the -able suffix.

    • For example, Comparable, Eatable, Reversible etc.

  2. Define a body-less function which uses the trait defined in step 1.

  3. Define some non-body-less function which uses the trait defined in step 1.

  4. Implement the required function needed by the traits for the data type we want

The following code demonstrates how to use trait in Pineapple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// First, define the trait
// Note that it is always in this format: 
//      def T is <TRAIT_NAME>
def T is Comparable


// Second, define a body-less function which uses the Comparable trait
// To achieve this, we need to use the `if` keyword
// This is like saying 
// we need to define a function for T called `.isMoreThan`
// if T is Comparable
def (this T).isMoreThan(that T) -> Boolean 
    if T is Comparable


// Thirdly, define the .max function which uses the Comparable trait
// We will use the `where` keyword here
def (this List{T}).max -> T
    where T is Comparable
    let mutable result = this.(0)
    for i in 1.to(this.length - 1)
        if this.(i).isMoreThan(result) // we can use the .isMoreThan function, because T is Comparable
            result = this.(i)
    return result

So, now we only left the last step. But, before that let's see what will happen if we missed out the last step:

1
2
3
let peoples = List{People}
let eldest = peoples.max 
// Error: `People` has not implemented the `Comparable` trait

We will get an error as described in line 3, because the People type have not implemented the function we defined in step 2. And here's how we can implement it.

1
2
3
def (this People).isMoreThan(that People) -> Boolean
    // if People is Comparable <-- This line is not needed!
    return this:age > that:age

And we are done.

So, if you ever need to use the max function on your custom data type, you just need to implement the isMoreThan function for your data type.

No more copy and paste!


Differences with other languages

In fact, Pineapple's trait is a featured inpsired by Java/C#'s Interfaces, Haskell's Type Classes and Scala's Trait.

However, there are one major differences that makes Pineapple's trait stands out:

Using trait in Pineapple will not break existing code.

Suppose we have a data type called Animal and a function named isMoreThan.

1
2
3
4
5
6
7
8
9
// Language:  Java
public class Animal {
    public String name;
    public int weight;

    public boolean isHeavierThan(Animal other) {
        return this.weight > other.weight;
    }
}
1
2
3
4
5
6
7
8
-- Language:  Haskell
data Animal = Animal {
    name   :: String,
    weight :: Int
}

isHeavierThan :: Animal -> Animal -> Bool
isHeavierThan x y = (weight x) > (weight y)
1
2
3
4
5
6
7
// Language:  Pineapple
def Animal
    :name    String
    :weight  Integer

def (this Animal).isHeavierThan(that Animal) -> Boolean
    return this:weight > that:weight

Imagine that during development, we suddenly realize we need a Comparable interface/typeclasses/trait for the Animal type. So, we coded it:

1
2
3
4
// Java
public interface Comparable<T> {
    public boolean isHeavierThan(T other);
}
1
2
3
-- Haskell
class Comparable a where
    isHeavierThan :: a -> a -> Bool
1
2
3
4
5
// Pineapple
def T is Comparable

def (this T).isHeavierThan(that T) -> Boolean
    if T is Comparable

However, to implement Comparable for Animal type, we need to modify the previous code as such (modification are those highlighted lines):

1
2
3
4
5
6
7
8
9
// Language:  Java
public class Animal implements Comparable<Animal> {
    public String name;
    public int weight;

    public boolean isHeavierThan(Animal other) {
        return this.weight > other.weight;
    }
}
1
2
3
4
5
6
7
8
-- Language:  Haskell
data Animal = Animal {
    name   :: String,
    weight :: Int
}

instance Comparable Animal where
    isHeavierThan x y = (weight x) > (weight y)

But, no modification is needed in the Pineapple's code!

1
2
3
4
5
6
7
// Language:  Pineapple
def Animal
    :name    String
    :weight  Integer

def (this Animal).isHeavierThan(that Animal) -> Boolean
    return this:weight > that:weight

One advantages of such feature is that Pineapple allows an easier incremental design approach, where you do not need to think of the future too much.


Other features

Trait extension

You can extend a trait by using the extends keyword. For example,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Define a trait named Equatable
def T is Equatable

def (this T) == (that T) -> Boolean
    if T is Equatable

def (this T) != (that T) -> Boolean 
    where T is Equatable
    return (this == that).not

def T is Comparable extends T is Equatable

def (this T) > (that T) -> Boolean
    if T is Comparable

def (this T) < (that T) -> Boolean
    where T is Comparable
    return ((this > that).not).and((this == that).not)

Using trait in data structures

Another application of trait is for creating generic data structure. For example, a BinaryTree:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def BinaryTree{T}
    where T is Comparable
    :current  T
    :left     BinaryTree{T}?
    :right    BinaryTree{T}?

def (this BinaryTree{T}?).insert(element T) -> BinaryTree{T}
    where T is Comparable

    if this == #nil
        this = BinaryTree{T}
            :current = element
            :left    = #nil
            :right   = #nil

    elif element >= this:current
        this:right = this:right.insert(element)

    elif element < this:current
        this:left = this:left.insert(element)

    return this

Multiple type parameters trait

To define a trait with more than one type parameter, you need to use the following format:

1
def T1 is <TRAIT_NAME> T2

For example,

1
2
3
4
def T1 is ComparableTo T2

def (this T1) > (that T2) -> Boolean
    if T1 is ComparableTo T2

Comments