Infix Notation ใน Kotlin คืออะไร

มีประโยชน์ยังไง แล้วเอาไปใช้ตอนไหน

Travis P
Black Lens
3 min readOct 11, 2018

--

นั่นมัน iflix!

Infix Notation คืออะไร

Infix notation เป็นรูปแบบหนึ่งของการเรียกฟังก์ชั่น ที่ละการเขียน . และ () ออกไป ซึ่งผมว่าพวกเราน่าจะใช้กันอยู่แล้วโดยไม่รู้ตัว ยกตัวอย่างเช่น

val myMap: Map<String,Int> = mapOf("one" to 1, "two" to 2)

ทุกครั้งที่เราใช้ helper function mapOf กับ to ก็เจ้า to นี่แหละครับ ที่เราเขียนในรูป infix ซึ่งหน้าตาแบบปกติของมันมี . มี () อย่างนี้

val myMap: Map<String,Int> = mapOf("one".to(1), "two".to(2))

วิธีประกาศฟังก์ชั่นให้เขียนแบบ infix ได้ ต้องแปะ keyword infix ไว้

infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

และฟังก์ชั่นนั้นต้องมีคุณสมบัติดังนี้

  1. เป็น member function หรือ extension function แปลว่า top level function ไม่เข้าข่าย ซึ่งก็มีเหตุผล เพราะมันต้องมีตัวหน้ากับตัวหลัง top level function มันไม่มีตัวหน้า
  2. ต้องมี 1 parameter เท่านั้น อันนี้ผมเดาว่าน่าจะผิดคำจำกัดความของ infix และน่าจะ parse ไม่ได้
  3. parameter ห้ามเป็น vararg และห้ามมี default value ถ้ามี vararg คือใส่ parameter เกิน 1 ตัวได้ ถ้ามี default value คือไม่ใส่ parameter ได้ ซึ่งละเมิดกฎข้อ 2

และยังมีข้อควรระวังอื่นๆตามไปอ่านได้ที่

นั่น Music ❤️

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

ขอสารภาพว่านึกอย่างอื่นไม่ออก นอกจาก

  1. สะดวก เคาะ space bar มันง่ายกว่าพิมพ์ .() แน่นอน
  2. อ่านง่าย ดูสะอาดตาขึ้นแน่ๆ แต่ก็ไม่ได้แปลว่ามันจะอ่านง่ายทุกกรณี

เอาไปใช้ตอนไหน

ถ้าตอบว่าเอาไปใช้ตอนสร้าง mapOf ก็จะอุบาทว์ไปหน่อย เอาเป็นว่าผมเล่าปัญหาให้อ่านดีกว่า แล้ว infix มันมาเป็นส่วนหนึ่งในการแก้ไขปัญหาได้ยังไง

คำเตือน เนื้อหาด้านล่างไม่ค่อยมีสาระนะครับ อ่านผ่านๆก็ได้

ผมเป็นคนที่ไม่ชอบทำงานซ้ำซากจำเจเป็นอย่างยิ่ง โดยเฉพาะการเขียน mapper ตาม CLEAN Architecture ที่ดี model ที่อาศัยกันอยู่คนละ layer จะมาข้าม layer กันไม่ได้ สมมติผมมี User ที่ต้องแปลงเป็น UserViewModel ก่อนจะเอาไปใช้แสดง ซึ่งส่วนใหญ่ผมก็ต้องนั่งแมพ field ต่อ field โง่ๆ หรือไม่ก็ต้องเลือกภาษาที่ถูกต้องไปแสดง

class UserMapper(val lang: String) {  fun mapUser(user: User) : UserViewModel {
return UserViewModel(
id = user.id,
name = localize(user.nameTH, user.nameEN),
p1 = localize(user.p1TH, user.p1EN),
p2 = localize(user.p2TH, user.p2EN)
)
}
fun localize(th: String, en: String) {
return if (lang == "TH") th else en
}
}

การแมพก็จะมีหน้าตาประมาณนี้ ซึ่งน่าเบื่อมาก ทำให้ผมต้องหา solution โดยด่วน

Solution 1: MapStruct

MapStruct เป็น java code generation library ที่ให้เราประกาศ interface แล้วเดี๋ยวมันจะ generate implementation ให้ โดยเราสามารถ custom ต่างๆนานาได้มากมาย ก็จะลดงานน่าเบื่อได้ส่วนหนึ่ง แน่นอนว่าบางส่วนก็ต้องเขียนเอง

แต่โลกแห่งความเป็นจริงมันช่างโหดร้ายครับ project lead ที่เป็น expert จากต่างประเทศและ senior ในทีมไม่อนุญาติให้ใช้ เพราะมันจะ break pattern ที่ใช้กันอยู่ ผมเองเป็นแค่ dev คนไทยโง่ๆคนนึง ก็ได้แต่ก้มหน้าก้มตาถอด library ออกไป

Solution 2: Copy code ที่ generate จาก MapStruct มาใช้

โอเค ไม่ใช้ MapStruct ก็ได้แต่ code ที่มัน generate เมื่อตะกี้อะ แยกไม่ออกจากมือเขียนเลยนะ ผมก็เลยสร้าง project มาอีกอัน เอาไว้ generate mapper โดยเฉพาะ! ตอนจะสร้างก็ copy ของมาจาก project หลัก generate mapper แล้วก็ copy กลับไปจนถึงตอนนี้ผมเชื่อว่ายังไม่มีใครรู้ ว่า mapper บางตัวเป็นคลาสที่ถูก generate ขึ้นมา

แต่โลกแห่งความเป็นจริงมันช่างโหดร้ายครับ บาง mapper มันก็มี dependency กับคลาสอื่น จะ copy มาทั้งยวงก็ลำบาก จะใช้ MapStruct แต่ไม่ใส่ git ก็ต้องระวังเป็นพิเศษ สุดท้ายก็ต้องล้มเลิกแผนนี้ไป

Solution 3: Android Studio Magic

Android Studio นี้มีสิ่งอำนวยความสะดวกเยอะครับ เช่น block selection ทำให้ผม copy & paste fields ได้เป็นชุดๆ หรือ duplicate cursor ที่ทำให้ผมพิมพ์หลายที่ได้ในเวลาเดียวกัน

ทีนี้พอทำงานกับ cursor หลายตัวในเวลาเดียวกัน ทำให้ code completion ใช้งานไม่ได้! คือมันจะ auto-complete อย่างถูกต้องให้กับ cursor หลัก ในขณะที่ cursor รองก็ได้ auto-complete ของอันหลักไปด้วย เจ๊งสิครับ ทำให้ผมต้อง copy & paste & พิมพ์เองทั้งหมด

Solution 4: Infix notation

การใช้ Android Studio Magic ก็ลดความปวดร้าวไปได้เยอะมากแล้ว แต่ก็ยังมีช่องให้ปรับปรุงเพิ่มได้อีก! ผมจึงทำการเปลี่ยน localize() ให้กลายเป็น extension function ของ String และเติม infix เข้าไปให้เรียกง่ายๆ

infix fun String.localize(en: String) {
return if (lang == "TH") this else en
}

เวลาใช้ให้เรียกอย่างนี้ แต่ต้องระวังว่า String ตัวแรกต้องเป็นภาษาไทยเสมอ

name = user.nameTH localize user.nameEN

ทำมาถึงจุดนี้มันสะดวกขึ้นอีกหน่อยนึงละ แต่มันอ่านไม่เป็นภาษาอังกฤษ ดังนั้งจึงทำการ rename localize เป็น or จนสุดท้ายกลายเป็นอย่างนี้

class UserMapper(val lang: String) {  fun mapUser(user: User) : UserViewModel {
return UserViewModel(
id = user.id,
name = user.nameTH or user.nameEN,
p1 = user.p1TH or user.p1EN,
p2 = user.p2TH or user.p2EN
)
}
infix fun String.or(en: String) {
return if (lang == "TH") this else en
}
}

เย้ สวยงาม พิมพ์ง่าย อ่านรู้เรื่อง ถือว่าตอบโจทย์(ที่ไม่ควรมีตั้งแต่แรก)มากๆ

สำหรับท่านที่ตกตะลึงพรึงเพริด เฮ้ย มาประกาศ extension function ในคลาสอื่นได้ไง แล้วมันจะมี this กี่อัน แล้วจะตีกันมั้ย ขอเชิญไปไขข้อข้องใจที่นี่ครับ

--

--

Travis P
Black Lens

Android Developer, Kotlin & Flutter Enthusiast and Gamer