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 thePeople
with the largestage
. -
For the
Fruit
list, the maximum means theFruit
with the largestsize
.
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:
-
Using lambdas (a.k.a first-class functions)
-
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?
-
Only people that understand lambdas understand the hackish looking code
-
Code maintainability is reduced (due to 1.)
-
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:
-
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.
-
-
Define a body-less function which uses the trait defined in step 1.
-
Define some non-body-less function which uses the trait defined in step 1.
-
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 |