Functional Programming ง่ายๆ สไตล์ Kotlin

Higher order function and Lambda explained!

Travis P
Black Lens
4 min readMay 20, 2017

--

เรื่องกล้วยๆ https://flic.kr/p/e6L23X

Functional Programming เป็นศาสตร์ที่ล้ำลึกยิ่งนัก ผมเองก็ยังไม่บรรลุทั้งหมด แต่เพราะ Kotlin นี่แหละ ที่เขียนๆไปก็เข้าใจ FP มากขึ้น วันนี้จะมาเล่า 3 คอนเซปของ FP ที่ปรากฏอยู่ใน Kotlin ก่อนครับ ประกอบด้วย first class function, higher order function และ pure function

First Class Function

ภาษาทุกภาษานั้นเขียนฟังก์ชั่นได้อยู่แล้วครับ แต่ไม่ใช่ทุกภาษาที่ถือว่าฟังก์ชั่นเป็นออบเจ็ค ห้ะ! ใช่แล้วครับ Kotlin มีตัวแปรประเภทฟังก์ชั่น เรามาดูตัวอย่างกัน

val myFunction: () -> Unit = { println("Hello Function") }myFunction() // Hello FunctionmyFunction.invoke() // Hello Function

() -> Unit คือฟังก์ชั่นประเภท ไม่รับค่า ไม่คืนค่า หรือเทียบได้กับ

fun myFunction() {
println("Hello Function")
}

เมื่อ myFunction เป็นตัวแปรประเภทฟังก์ชั่นแล้วmyFunction() ก็คือการเรียกฟังก์ชั่นนั่นเอง หรือถ้าอยากให้ชัดเจน เราก็สามารถเรียกฟังก์ชั่น myFunction.invoke() บนตัวแปรประเภทฟังก์ชั่นได้ ซึ่งก็เหมือนการ method call ปกติ ใช่แล้วครับ! ตัวแปรประเภทฟังก์ชั่นมีฟังก์ชั่นชื่อ invoke ไว้เรียก ฟังเซปชั่นจริงๆ (ฟังก์ชั่น + อินเซปชั่น)

Lambda

นิยามของ lambda คือ function literal ครับ อ้าวแล้ว function literal คืออะไร
มันคือหน้าตาของฟังก์ชั่นครับ
something literal คือหน้าตาของสิ่งนั้นแหละ ผมขอยกตัวอย่างดังนี้

"This is a String" // String literal1024 // Int literal65.536 // Double literal{ param -> println("inside a function" } // function literal

เราเห็นตัวหนังสือใน double quote "" เรารู้ว่ามันคือ String
เราเห็นตัวเลข เรารู้ว่ามันคือ Int
เราเห็นเลขมีจุด เรารู้ว่ามันคือ Double
เช่นเดียวกัน เราเห็นตัวแปร ลูกศร และโค้ดในปีกกา เรารู้ว่ามันคือ function

val biFunction: (Int, String) -> String = { i: Int, s: String ->
"2 parameters, Int = $i, String = $s"
}
val message = biFunction(5, "Hola")
println(message)
// 2 parameters, Int = 5, String = Hola
println(biFunction.invoke(18, "Wow"))
// 2 parameters, Int = 18, String = Wow

(Int, String) -> String คือฟังก์ชั่นประเภทรับค่า Int รับ String คืน String หรือเทียบได้กับ

fun biFunction(i: Int, s: String) {
return "2 parameters, Int = $i, String = $s"
}

{ i: Int, s: String -> "..." } คือ lambda ที่รับ Int รับ String คืน String
เวลาใช้ lambda syntax เราไม่ต้องเขียน return มันจะ return statement สุดท้ายให้เอง ในกรณีนี้ statement สุดท้ายเราเป็น String มันก็จะ return String ออกมา

ในกรณีที่ compiler รู้ว่า type ของ parameters คืออะไร เราสามารถละ parameter type ใน lambda ได้

val biFunction: (Int, String) -> String = { i, s ->
"2 parameters, Int = $i, String = $s"
}

ถ้าฟังก์ชั่นเรารับ 1 parameter ใน lambda เราไม่จำเป็นต้องตั้งชื่อตัวแปรก็ได้ Kotlin มีตัวแปรชื่อ it มาให้เราอยู่แล้ว

val fun1: (Int) -> Unit = { println("it is an Int with value $it") }val fun2: (String) -> Unit = { 
println("it is a String with value $it")
}
println(fun1(38))
// it is an Int with value 38
println(fun2("Hello Function"))
// it is a String with value Hello Function

First Class Function ใน Java 6

เป็นที่รู้กันว่า Java 6 ไม่ support functional programming แต่ถ้าเราลองเอาไอเดีย first class function มาทำเองล่ะ จะเป็นยังไง

// () -> Unit
interface Function {
void invoke();
}
// (Int, String) -> String
interface ISSFunction {
void invoke(Int i, String s);
}
Function f1 = new Function() {
@Override
void invoke() {
System.out.println("Hello Java 6");
}
}
ISSFunction f2 = new ISSFunction() {
@Override
String invoke(Int i, String s) {
return String.format("Int = %d, String = %s");
}
}
f1.invoke();
// Hello Java 6
String message = f2.invoke(7, "Haha");
System.out.println(message)
// Int = 7, String = Haha

เราก็คงต้องใช้ interface แทน function type และใช้ anonymous class แทน lambda กันแหละครับ

แต่ถ้าจะให้มานั่งประกาศสำหรับทุก type ก็ลำบากไป อาจจะใช้ Generic ช่วยได้ดังนี้ครับ

interface BiFunction<P1,P2,R> {
R invoke(P1 p1, P2 p2);
}
BiFunction<Int, Double, String> f = new BiFunction<>() {
@Override
String invoke(Int p1, Double p2) {
return String.format("p1 = %d, p2 = %.2f");
}
}
String message = f.invoke(79, 12.0);
System.out.println(message);
// p1 = 79, p2 = 12.00

Higher Order Function

นิยามของมันคือ ฟังก์ชั่นที่รับฟังก์ชั่นเป็นพารามิเตอร์ หรือคืนค่าเป็นฟังก์ชั่น หรือทั้งรับและคืนค่าเป็นฟังก์ชั่น เป็นคอนเซปที่ทรงพลังมากๆ ใน Kotlin เรามักเห็นแบบรับค่าเป็นฟังก์ชั่นซะมากกว่า

fun getStringFromNetwork(callback: (String) -> Unit) {
val string: Srtring = "String from network"
callback(string)
}
getStringFromNetwork ({ println(it) })
// String from network
getStringFromNetwork { println(it) }
// String from network

getStringFromNetwork จัดเป็น higher order function รับตัวแปรประเภทฟังก์ชั่นที่รับ String ไม่คืนค่า

ตอนเรียกใช้เราก็มักเรียกใช้ พร้อมใส่ lambda เข้าไป

ถ้าเราส่ง lambda เป็นพารามิเตอร์สุดท้าย เราสามารถเอา lambda ออกมานอกวงเล็บได้เพื่อความสวยงาม

ทั้งนี้ไม่เกี่ยวกับว่าเราจะทำงาน async หรือไม่ ใช้งานได้หมด แต่ที่ยกตัวอย่าง callback เพราะเห็นได้ชัดดี เรามาลองดูอีกตัวอย่างที่ซับซ้อนขึ้น

fun introduce(name: String, sentence: (String) -> String): String {
return sentence(name)
}
val message = introduce("Travis P") { "Hi, my name is $it." }
println(message)
// Hi, my name is Travis P.

Higher Order Function ใน Java 6

interface Function<T,R> {
R invoke (T p1);
}
void getStringFromNetwork(Function<String,Void> callback) {
String string = "String from network";
callback.invoke();
}
getStringFromNetwork(new Function<String,Void>() {
@Override
Void invoke(String p1) {
System.out.println(p1);
return null;
}
}
// String from network
String introduce(String name, Function<String,String> sentence) {
return sentence.invoke(name);
}
String message = introduce("Travis P",
new Function<String,String>() {
@Override
String invoke(String p1) {
return String.format("Hi, my name is %s.", p1);
}
}
);
System.out.println(message);
// Hi, my name is Travis P.

ประโยชน์?

หลักๆเลยคือทำให้เราเขียนโค้ดกระชับ เข้าใจง่าย และที่สำคัญ Kotlin standard library มีของเล่นมากมาย ตามไปดูตัวอย่างได้เลย

พิเศษ! หากคุณเขียน Kotlin ร่วมกับ Java วันนี้

คุณสามารถใส่ lambda แทน interface callback จากฝั่ง Java ได้ ยกตัวอย่างเช่น

view.setOnClickListener(new View.OnClickListener() {
@Override
void onClick() {
//...
}
});

สามารถเขียนใน Kotlin ได้แค่นี้ view.setOnClickListener { /*...*/ }
ฟีเจอร์แถมนี้เรียกว่า SAM conversion

Pure Function

ความหมายของอันนี้คือ ฟังก์ชั่นนี้ ถ้าส่งพารามิเตอร์เดิมเข้าไป ต้องคืนค่าเดิมออกมาเสมอ เช่น

fun add(i1: Int, i2: Int): Int {
return i1 + i2
}

ส่วนอันนี้ไม่ pure

class NonPure {
var number: Int = 0
fun add(i1: Int, i2: Int): Int {
return number + i1 + i2
}
}
val nonPure = NonPure()println(nonPure(1,2))
// 3
nonPure.number = 5
println(nonPure(1,2))
// 8

pure function ใช้ได้แค่สิ่งที่ส่งเข้ามาเท่านั้น จะไม่มีการไปดึงค่าจากที่อื่นมาใช้ ยกเว้นกรณีเป็นค่าคงที่ เช่น Math.PI สามารถใช้ในฟังก์ชั่นได้
pure function จะไม่ไปแก้ไขตัวแปรอื่นนอกฟังก์ชั่น

เรื่องนี้มันไม่เป็นข้อบังคับของ Kotlin ครับ แต่เป็น practice เราเลือกปฏิบัติเอง

ตรงนี้มีประโชยน์ยังไง?

มัน predictable ครับ ถ้า input เป็นงี้ output ต้องเป็นงี้ทุกครั้ง ไม่เกี่ยวกับ state ข้างนอกเลย ข้อดีอีกอย่างคือทำ test ง่าย

https://flic.kr/p/9K7zvi

สรุป

Functional Programming เป็นคอนเซปที่ทรงพลังครับ ถ้าเข้าใจแล้วจะเขียนโค้ดได้กระชับขึ้น เข้าใจง่าย สวยงาม อาจต้องทำความเข้าใจคอนเซปใหม่นิดนึง แต่ก็ไม่ยากเกินความสามารถพวกเราแน่นอน โค้ด Kotlin อาจดูล้ำ แต่ไม่มี magic โค้ดทุกอย่างอธิบายได้ มีที่ไปที่มา สามารถเขียนได้ด้วย Java ธรรมดาทั้งหมด

FP ยังไม่หมดเพียงเท่านี้นะครับ วันนี้เอาเท่านี้ก่อน เรามาร่วม #ทีมKotlin กันครับ

--

--

Travis P
Black Lens

Android Developer, Kotlin & Flutter Enthusiast and Gamer