Multi-Paradigm
The Cangjie language is a typical multi-paradigm programming language. It provides support for procedural programming, object-oriented programming, and functional programming, covering value types, classes and interfaces, generics, algebraic data types, and pattern matching, and for features such as treating functions as first-class citizens.
Classes and Interfaces
Cangjie supports object-oriented paradigm programming using traditional classes and interfaces. The Cangjie language supports only single inheritance. Each class has only one parent class, but can implement multiple interfaces. Each class is a subclass (direct subclass or indirect subclass) of Object
. In addition, all Cangjie types (including Object
) implicitly implement the Any
interface.
Cangjie provides the open
modifier to control whether a class can be inherited or whether an object member function can be overridden by a subclass.
In the following example, class B inherits class A and implements interfaces I1 and I2. The declaration of class A must be modified by open
so that class A can be inherited. Function f
in class A is also modified by open
and can be overridden in class B. For a call of function f
, the version to be executed is determined based on the object type, which is called dynamic dispatch.
open class A {
let x: Int = 1
var y: Int = 2
open func f(){
println("function f in A")
}
func g(){
println("function g in A")
}
}
interface I1 {
func h1()
}
interface I2 {
func h2()
}
class B <: A & I1 & I2 {
override func f(){
println("function f in B")
}
func h1(){
println("function h1 in B")
}
func h2(){
println("function h2 in B")
}
}
main() {
let o1: I1 = B()
let o2: A = A()
let o3: A = B()
o1.h1() // "function h1 in B"
o2.f() // "function f in A"
o3.f () // Dynamic dispatch, "function f in B"
o3.g() // "function g in A"
}
Cangjie interfaces can inherit each other and are not restricted by single inheritance. That is, one interface can inherit multiple parent interfaces. In the following example, I3
can inherit both I1
and I2
. Therefore, implementations of functions f
, g
, and h
must be provided to implement I3
.
interface I1 {
func f(x: Int): Unit
}
interface I2 {
func g(x: Int): Int
}
interface I3 <: I1 & I2 {
func h(): Unit
}
Treating Functions as First-Class Citizens
In Cangjie, a function can be used as a common expression, passed as a parameter, used as a function return value, stored in other data structures, or used to assign a value to a variable.
func f(x: Int) {
return x
}
let a = f
let square = {x: Int = > x * x} // Lambda expression
// Defines nested functions and uses these functions as return values.
func g(x: Int): () -> Int {
func h(){
return f(square(x))
}
return h
}
func h(f: ()->Int) {
f()
}
let b = h(g(100))
In addition to the global functions in the preceding example, member functions of data types such as object or struct can also be used as first-class citizens. In the following example, the member function resetX
of object o
, as a common expression, is assigned to variable f
. The call of f
changes the value of member variable x
in object o
.
class C{
var x = 100
func resetX(n: Int){
x = n
return x
}
}
main(){
let o = C()
let f = o.resetX // Treats the member function as a first-class citizen.
f(200)
print(o.x) // 200
}
Algebraic Data Types and Pattern Matching
An algebraic data type is a composite type that consists of other data types. Two common algebraic types are product type (such as struct and tuple) and sum type (such as tagged union).
The following focuses on the sum type enum
in Cangjie and the corresponding pattern matching capability.
In the following example, BinaryTree of the enum type has two constructors: Node
and Empty
. Empty
does not have any parameter, and corresponds to a binary tree with only one empty node. Node
requires three parameters to construct a binary tree with one value and left and right subtrees.
enum BinaryTree {
| Node(Int, BinaryTree, BinaryTree)
| Empty
}
Before being accessed, the values of these enum
instances must be parsed using pattern matching. Pattern matching is a method for testing whether an expression has a specific feature. Cangjie provides the match
expression to implement this method. For a given expression of the enum
type, the match
expression is used to determine which constructor is used to construct the given expression and to extract the parameters of the corresponding constructor. In the following example, the recursive function sumBinaryTree
is used to sum the integers stored in the binary tree node.
func sumBinaryTree(bt: BinaryTree): Int {
match (bt) {
case Node(v, l, r) =>
v + sumBinaryTree(l) + sumBinaryTree(r)
case Empty => 0
}
}
In addition to the enum
mode, Cangjie provides the constant mode, binding mode, type mode, and nested use of various modes. The following example describes the use of the modes provided by Cangjie.
- Constant mode: Use multiple literal values, such as integers and strings, for equality check.
- Binding mode: Bind a member at a specified location to a new variable. This mode is mainly used to deconstruct instances of the enum or tuple type. In the preceding example of
sumBinaryTree
, the binding mode is used to bind the actual parameters in theNode
node to the three newly declared variablesv
,l
, andr
. - Type mode: Check the subtype relationship between the variable type and target type. This mode is mainly used to convert a type to its subtype.
- Tuple mode: Compare or deconstruct instances of the tuple type.
- Wildcard mode: Match any value.
Cangjie plans to introduce more modes in the future, such as sequence mode and record mode.
// Constant mode: string literal
func f1(x: String) {
match (x) {
case "abc" => ()
case "def" => ()
case _ => () // Wildcard mode
}
}
// tuple mode
func f2(x: (Int, Int)) {
match (x) {
case (_, 0) => 0 // Wildcard mode and constant mode
case (i, j) => i / j // Binding mode, in which the element of **x** is bound to variables **i** and **j**
}
}
// Type mode
func f3(x: ParentClass) {
match (x) {
case y: ChildClass1 => ...
case y: ChildClass2 => ...
case _ => ...
}
}
Generics
In modern software development, generic programming has become a key technology to improve code quality, reusability, and flexibility. As a parameterized and polymorphic technology, generics allows developers to use types as parameters when defining types or functions to create common code structures that apply to different data types. The benefits of generics include:
- Code reuse: Common algorithms and data structures used to perform operations on different types can be defined to reduce code redundancy.
- Type security: More type checks during compilation are supported to prevent type errors during running, thereby enhancing program stability.
- Performance improvement: Program execution efficiency can be improved by avoiding unnecessary type conversion.
Cangjie supports generic programming. Generic variables can be introduced to functions, structs, classes, interfaces, and extends to implement function generalization. The array type is typical generic type application in Cangjie. Its syntax is Array<T>
, where T indicates the element type and can be instantiated as any specific type, for example, Array<Int>
or Array<String>
, or even a nested array Array<Array<Int>>
, so that arrays of different element types can be easily constructed.
In addition to types, generic functions can also be defined. For example, a generic function can be used to implement the concat operation on any two arrays of the same type. As shown in the following code, a generic function concat is defined which supports any two array parameters of the Array<T>
type. After processing, and a new array is returned. In this way, the concat function can be applied to arrays of any type besides Array<Int>
, Array<String>
, and Array<Array<Int>>
.
func concat<T>(lhs: Array<T>, rhs: Array<T>): Array<T> {
let defaultValue = if (lhs.size > 0) {
lhs[0]
} else if (rhs.size > 0) {
rhs[0]
} else {
return []
}
let newArr = Array<T>(lhs.size + rhs.size, item: defaultValue)
// Copies the entire segment of continuous memory pointed to by the right expression to the entire segment of continuous memory pointed to by the left array slice.
newArr[0..lhs.size] = lhs
newArr[lhs.size..lhs.size+rhs.size] = rhs
return newArr
}
The combination of generics, interfaces, and subtypes enables specific constraints on type variables in generics, thereby limiting the types that can be filled in the type variables. In the following example, the element element
needs to be searched for in the array arr
. Although the specific types of the array and its elements are not considered, the element type T
must support operations such as equality check to check whether any element in the array is equal to the given element. Therefore, in the where
clause of the lookup
function, T <: Equatable<T>
, that is, the type T
, must implement the Equatable<T>
interface.
func lookup<T>(element: T, arr: Array<T>): Bool where T <: Equatable<T> {
for (e in arr){
if (element == e){
return true
}
}
return false
}
The generic type of Cangjie does not support covariance. For example, arrays containing different types of elements are completely different and do not support value assignment to each other or parent-child relationships between element types. This prevents the type insecurity problem caused by array covariance.
In the following example, Apple is a subclass of Fruit, but value assignment is not supported between variables a and b, and there is no parent-child relationship between Array<Fruit>
and Array<Apple>
.
open class Fruit {}
class Apple <: Fruit {}
main() {
var a: Array<Fruit> = []
var b: Array<Apple> = []
a = b // Reports a compilation error.
b = a // Reports a compilation error.
}