Other Modern Features and Syntactic Sugar

Function Overloading

In the Cangjie language, functions with the same name are supported in the same scope. The compiler determines the function to be executed based on the number and types of parameters. For example, the following absolute value functions provide implementations different numeric types. These implementations have the same function name abs, making function calls easier.

func abs(x: Int64): Int64 { ... }
func abs(x: Int32): Int32 { ... }
func abs(x: Int16): Int16 { ... }
...

Named Parameters

When a function with named parameters is called, in addition to the actual parameter expression, names of the corresponding formal parameters must be provided. Named parameters help improve program readability, reduce the sequence dependency of parameters, and simplify program expansion and maintenance.

In the Cangjie language, developers can define a named parameter by adding ! after the formal parameter name. After a formal parameter is defined as a named parameter, the parameter name must be specified before the actual parameter value when the function is called.

func dateOf(year!: Int, month!: Int, dayOfMonth!: Int) {...}

dateOf(year: 2024, month: 6, dayOfMonth: 21)

Default Value

For functions in Cangjie, default values can be provided for specific formal parameters. When a function is called, if the default value of a parameter is used as the actual parameter, the parameter can be omitted. This feature reduces the need for function overloading or the builder pattern, thereby reducing code complexity.

func dateOf(year!: Int64, month!: Int64, dayOfMonth!: Int64, timeZone!: TimeZone = TimeZone.Local) {
    ...
}

dateOf(year: 2024, month: 6, dayOfMonth: 21) // ok
dateOf(year: 2024, month: 6, dayOfMonth: 21, timeZone: TimeZone.UTC) // ok

Trailing Lambda

Cangjie supports trailing lambdas, which make it easier to implement specific syntax in DSL. Specifically, many languages provide the following classic built-in conditional judgment or loop code blocks:

if (x > 0) {
    x = -x
}

while (x > 0) {
    x--
}

Trailing lambda allows DSL developers to customize code block syntax similar to those built in the host language. The following is an example of a function call by using trailing lambda in the Cangjie language:

func unless(condition: Bool, f: ()->Unit) {
    if(!condition) {
        f()
    }
}

let a = f(...)
unless(a > 0) {
    print("no greater than 0")
}

The call of the unless function seems to be a special if expression. This syntax effect is implemented by using the trailing lambda syntax. If the last formal parameter of a function is of the function type, when the function is called, a lambda expression can be provided as the actual parameter and written outside the function call parentheses. Especially when the lambda expression is a function without parameters, the double arrows (=>) in the lambda expression can be omitted and represented as code blocks to further reduce the syntactic noise in the corresponding DSL. Therefore, in the preceding example, the second actual parameter in the unless function call becomes the following lambda expression:

{ print("no greater than 0") }

If the function has only one formal parameter which is of the function type, the function call parentheses can be omitted when trailing lambda is used to call the function, making the code simpler and more natural.

func runLater(fn:()->Unit) {
    sleep(5 * Duration.Second)
    fn()
}

runLater() { // OK
    println("I am later")
}

runLater { // The parentheses can be omitted.
    println("I am later")
}

Pipeline Operators

Cangjie introduces pipeline operators to simplify the syntax of calling nested functions and to express data flows more intuitively. The following example shows nested function calls and the equivalent expression based on the pipe operator |>. The latter reflects the data flow more intuitively. The value of the expression on the left of |> is transferred as a parameter to the function on the right.

func double(a: Int) {
    a * 2
}

func increment(a: Int) {
    a + 1
}

double(increment(double(double(5)))) // 42

5 |> double |> double |> increment |> double // 42

Operator Overloading

Cangjie defines a series of operators represented by special characters. Most of the operators can be overloaded and used on types defined by developers, providing simple and intuitive syntax expressions for operations of user-defined types.

In Cangjie, developers only need to define operator overloading functions to implement operator overloading. In the following example, a type Point is defined to represent a point in a two-dimensional plane and the + operator is overloaded to define the addition of the two points.

struct Point {
    let x: Int
    let y: Int

    init(x: Int, y: Int) {...}

    operator func +(rhs: Point): Point {
        return Point(
            this.x + rhs.x,
            this.y + rhs.y
        )
    }
}

let a: Point = ...
let b: Point = ...
let c = a + b

Property

In the object-oriented paradigm, member variables are usually declared as private, and accesses to member variables are encapsulated into two public methods: getter and setter. In this way, data access details can be hidden, making it easier to implement service policies such as access control, data monitoring, tracing and debugging, and data binding.

Properties, a special kind of syntax, are provided in Cangjie. Like a member variable, a property can be accessed and supports value assignment. In addition, getter and setter methods are provided for various data operations. The access and assignment of values to properties are translated by the compiler into calls of the corresponding getter and setter member functions. Specifically, prop is used to declare a read-only property. A read-only property only supports a getter, and therefore the get implementation must be provided. mut prop is used to declare a mutable property. Mutable properties have a getter and a setter. Therefore, the get and set implementations must be provided.

As shown in the following example, if a developer wants to record accesses to each data member of the Point type, the developer can declare member variables modified by private in the Point type, allow external accesses by declaring the corresponding properties, and use the log system Logger to record access information. For users, using properties of object p is the same as accessing its member variables but with the recording function implemented internally. In the example, x and y are read-only and therefore only the get implementation is provided. color is declared with mut prop and is variable, therefore both get and set implementations are provided.

class Point {
    private let _x: Int
    private let _y: Int
    private var _color: String

    init(x: Int, y: Int, color: String) {...}

    prop x: Int {
        get() {
            Logger.log(level: Debug, "access x")
            return _x
        }
    }

    prop y: Int {
        get() {
            Logger.log(level: Debug, "access y")
            return _y
        }
    }

    mut prop color: String {
        get() {
            Logger.log(level: Debug, "access color")
            return _color
        }

        set(c) {
            Logger.log(level: Debug, "reset color to ${c}")
            _color = c
        }
    }
}

main() {
    let p = Point(0, 0, "red")
    let x = p.x // "access x"
    let y = p.y // "access y"
    p.color = "green" // "reset color to green"
}