Lambdas with Receivers

Lambdas with receivers in Kotlin.

fun main() {

    println(countTo100())
    println(countTo99().toString()) // Call toString() here, on the returned StringBuilder object

    val employee = KotlinEmployee2("Jim", "Johnson", 2011)
    val employees = listOf(KotlinEmployee2("John", "Smith", 2012), KotlinEmployee2("Jane", "Wilson", 2015), KotlinEmployee2("Mary", "Johnson", 2010), KotlinEmployee2("Mike", "Jones",2002))

    // Let's pretend we don't know about the find() function in Collections
    findByLastName(employees, "Wilson")
    findByLastName(employees, "Watson")
    findByFirstName(employees, "Jane")
    findByFirstName(employees, "Joan")

    with (employee.firstName) { // Regular usage of 'with' function
        println(capitalize())
    }

    myWith(employee.firstName) {// Demo: This is a high-order function. It passes a function into a function
        println(capitalize())
    // Every time this is called a lambda object is created. This could result in excessive memory consumption. To avoid this, make the myWith function inline

        println(employee.run { firstName }) // Another example of an extension lambda which works with all data types. Takes one lambda as argument and returns the result of executing the lambda. Sends in 'this' as an argument, but the 'this' can be excluded when calling e.g. 'name' instead of 'this.name'.

        DbConnection.getConnection().let { connection -> ..... } // Connection will no longer be available outside the block, limiting its scope.
// .let can also be used as an alternative to testing against null:
variable?.let { someFunction } // This block will be executed if 'variable' is not null. 'it' (variable) has now also been cast to not-null
// It is also particularly useful for chaining manipulations together:
        println((employee.let{it.firstName.capitalize()} // Capitalise first name
                         .let{it + " Snr"} // Concatenate some text
                         .let{it.length} // Get its length
                         .let{it + 31}) // And add 31. Returns 38

        println(employee.apply {}) // Another example of an extension lambda which works with all data types. Returns the object it's applied to rather than the result of the execution. Really useful for calling functions on a newly created object:
        val employee2 = KotlinEmployee2(name = "Jack").apply{ name = "James" } // Immediately call a function on a newly created object
        println(employee2.name)

    "Some String".apply somestring@{ // Add label so we can apply an operation to it
        "Another String".apply {
            println(toLowerCase()) // Operates on "Another String"
            // If we want to perform an operation on "Some String" use labels:
            println(this@somestring.toUpperCase())
        }
    }


}

// The long way
//fun countTo100(): String {
//    val numbers = StringBuilder()
//    for (i in 1..99) {
//        numbers.append(i)
//        numbers.append(",")
//    }
//    numbers.append(100)
//    return numbers.toString() // numbers variable is used a lot
//}

fun countTo100() = with(StringBuilder()) { // 'with' converts the instance you pass to it into a RECEIVER
        for (i in 1..99) {
            append(i) // We can exclude the instance reference
            append(",") // Could also type this.append but less concise
        }
        append(100)
        toString()
    }

Further explanation:

// EXTENSION LAMBDA - this is the equivalent of the built-in 'with' function:
fun myWith(name: String, block: String.() -> Unit) { // block (common convention name, can be anything) is the name of the function definition for the operation. We use this name in the body of the myWith function
    // String.() specifies the class we are extending (the Receiver object), so that we can use myWith on it. Unit is the return type of this particular function i.e. no return type
    // So block is now an extension function on a String object, and it can be applied to a String. We can now apply the passed-in function to the passed-in argument.
    name.block()
    // When we call the myWith function on fish.name (above), fish.name is the name argument and capitalize() is the block function
    // capitalize() returns a copy of the passed-in String, and does not change the original String. Wrapping it in a printLine shows us the result
}

// 'apply' does the same as 'with' but always returns the receiver object

fun countTo99() = StringBuilder().apply {
    for (i in 1..98) {
        append(i) // We can exclude the instance reference
        append(",") // Could also type this.append but less concise
    }
    append(99) // Don't call toString() here as the whole StringBuilder object is returned
} // You could add .toString() here as well

// The long way
fun findByLastName(employees: List<KotlinEmployee2>, lastName: String) {
    for (employee in employees) {
        if (employee.lastName == lastName) {
            println("We have an employee with last name $lastName")
            return
        }
    }
    println("Nobody here with the last name $lastName")
}

fun findByFirstName(employees: List<KotlinEmployee2>, firstName: String) {
    employees.forEach { // We can use a lambda as this is a functional interface
        if (it.firstName == firstName) {
            println("we have an employee with first name $firstName")
            return // Returns from both the lambda and the function i.e. a non-local return. This will only work when the function that is taking the lambda is inlined
            // If we want to make it a local return we can label the lambda: employees.forEach returnBlock@ {     followed by    return@returnBlock. In this case it would result in the "Nobody..." String being printed
            // This is useful when using nested 'with' or 'apply' statements (see "Some String" example in main function)
        }
    }
    println("Nobody here with first name $firstName")
}

data class KotlinEmployee2(val firstName: String, val lastName: String, val startYear: Int) { // Data class overrides our toString for a legible output

}

 

Lambdas

Lambdas in Java.

// Easier way to work with interfaces that have just one method that has to be implemented (functional interfaces). Also true if more than one method but other(s) have default implementation
// Often used in place of anonymous classes
public class Lambdas {

    public static void main(String[] args) {
        new Thread(new CodeToRun()).start(); // One method

        new Thread(new Runnable() { // Another method - anonymous class. Still a lot of code to run one line of output
            @Override
            public void run() {
                System.out.println("Also printing from the Runnable");
            }
        }).start();

        // Can be replaced with a lambda
        new Thread(() -> System.out.println("Still printing from the Runnable")).start(); // Passes the println() directly to the Thread constructor
        // 3 parts to a lambda - 1. argument list (empty in this case) 2. arrow token 3. body (put code to be executed here) - use {} for multiple lines (remember ; when multiple lines):

        new Thread(() -> {
            System.out.println("Line one");
            System.out.println("Line two");
        }).start();

        Employee john = new Employee("John Doe", 30);
        Employee tim = new Employee("Tim Buchalka", 21);
        Employee jack = new Employee("Jack Hill", 40);
        Employee snow = new Employee("Snow White", 22);

        List<Employee> employees = new ArrayList<>();
        employees.add(john);
        employees.add(tim);
        employees.add(jack);
        employees.add(snow);

//        Collections.sort(employees, new Comparator<Employee>() {
//            @Override
//            public int compare(Employee employee1, Employee employee2) {
//                return employee1.getName().compareTo(employee2.getName());
//            }
//        });

        Collections.sort(employees, (employee1, employee2) -> employee1.getName().compareTo(employee2.getName()));

        for (Employee employee : employees) {
            System.out.println(employee.getName());
        }

//        String sillyString = doStringStuff(new UpperConcat() {
//            @Override
//            public String upperAndConcat(String s1, String s2) {
//                return s1.toUpperCase() + s2.toUpperCase();
//            }
//        }, employees.get(0).getName(), employees.get(1).getName());
//        System.out.println(sillyString);

        String sillyString = doStringStuff((s1, s2) -> s1.toUpperCase() + s2.toUpperCase(), employees.get(0).getName(), employees.get(1).getName()); // No return keyword required
        System.out.println(sillyString);

    }
    //What if the interface returns a value?
    public final static String doStringStuff(UpperConcat uc, String s1, String s2) {
        return uc.upperAndConcat(s1, s2);
    }
}

class CodeToRun implements Runnable {
    @Override
    public void run() {
        System.out.println("Printing from the Runnable");
    }
}

class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

interface UpperConcat {
    public String upperAndConcat(String s1, String s2);
}