2019年7月28日 星期日

理解 Kotlin 的 companion object (變身) vs instance of class (化身) 之不同

在 Kotlin 中可以將類型與類型建構式分別指定給變數, 到底有何不同, 看以下例子:
class AAA { // 類型 AAA,  預設的建構式就是 AAA( )
    var b = 3 // 這是待化身的變數宣告, 透過建構式 AAA( ) 化身成物件成員(instance member)
    companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
        var a  = 5  // 需透過 AAA 存取成員
        var b = 10 // 名字同化身的成員, 直接變身成物件成員, 必需透過 AAA 存取成員
    }
}
fun main(args: Array < String > ) {
    val c = AAA    // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
    val i  = AAA( ) // i 才是類型的化身, 透過類型建構式 ( ) 才能化身成物件(instance of class)
    println(c.a)    // a 是 AAA 共享成員
    println(c.b)    // b 也是 AAA 成員之一
    println(i@AAA.a) // i 要透過變身標籤 @AAA 才能存取共享成員 a
    println(i@AAA.b) // i 要透過變身標籤 @AAA 才能存取共享成員 b
    // println(i.a)  //  錯誤的用法: a 實際並非 i 的成員
    // println(i.b)  //  錯誤的用法: b 實際並非 i 的成員
    println(i.b)     //  這裡的 b 是透過建構式生成的, 並非變身(companion object)裡面的 b
執行結果:
5
10
5
10
3
變身與化身中文意義是相同的, 如果把英文的 companion object(協同物件)翻譯成"變身", 而將 instance of class (類型物件)翻譯成"化身"時, 兩者本質上都是物件, 就要區分它們的不同, 也許可以這樣理解: 一旦宣告了協同物件(簡稱變身), 類型名稱可以變身(ailias of companion object)成物件去存取共享成員, 而類型物件(instance of class)必需透過建構式去化身, 兩者物件的生成方式不同在於: 變身在是在編譯階段(compile phase)生成的靜態物件(static object),從程式開始執行到結束生命都一直維係著不會消失, 但化身是在執行階段(runtime)才生成的區域物件(scope object), 生命周期受到控制, 一旦離開所在的生命區域(scope)便消失殆盡. 變身主要目的是維繫共享資源, 實際上只有一份變身物件存在類型內,  所有類型的化身並沒有變身這個物件!如果真要在化身物件方便取用,除了用 @標籤外, 也可以用 set() 及 get() 刻意連結起來,只不過要注意的是, 因為是共享的(static object),若在多工環境下也許要用 semaphore 來加以鎖定.
class AAA { // 類型 AAA,  預設化身的建構式就是 AAA( )
    companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
          var a  = 5  //  AAA 變身的成員
          var b = 10 // 若名稱與化身的成員相同時, 必需透過 AAA 存取成員
    }
    var b: Int  get( )         {  return AAA.b } // 符號讀取連結, 讓化身間接讀取變身成員 b
                     set(v: Int) {  AAA.b = v      } // 符號寫入連結, 讓化身間接寫入變身成員 b
    fun a( ) { // 當化身成員名稱與變身的成員不同時, 化身可以取用到變身的成員只是因為內定(default)符號聯結的關係, 並不表示他就是化身的成員
         return a  // 等同取用 AAA.a , 只是因為 kotlin 內定會將符號 a 聯結到變身成員 AAA.a
    }
}
fun main(args: Array < String > ) {
    val c = AAA    // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
    val i  = AAA( ) // i 是類型的化身, 透過類型建構式 ( ) 化身成物件(instance of class)
    println(c.a)    // a 是 AAA 共享成員
    println(c.b)    // b 也是 AAA 成員之一
    println(i.a())  // 呼叫函式 a( )間接讀取 AAA.a
    println(i.b)    // 已經刻意將 b 做了符號讀取連結, 看似直接(實際是間接)讀取
    i.b = 3           // 已經刻意將 b 做了符號寫入連結, 看似直接(實際是間接)寫入
    println(c.b)    // 因為 b 是共享成員, 所以變身的成員也跟著改變
     
}
結果:
5
10
5
10
3

2019年7月26日 星期五

Kotlin 的 promise

若要用 Kotlin 寫 Javascript (Kotlin to JS), 使用 promise 要先 import kotlin.js.Promise,  接著宣告資料型態, 一般用法像是:
    import kotlin.js.Promise
    import kotlin.browser.*
    fun main() {   
           lateinit var resolve: (Int)->Unit
           val promise = Promise < Int > { Yes, _ ->
                resolve = Yes
           }
           promise.then { result ->    // only execute when promise fullfill
                println("I got $result")
           }
           window.setTimeout( {resolve(0)}, 2000)  // resolve result sometime or somewhere
    }
例如應用在按鍵事件驅動的不定時的異步化(async function)程式上相當實用.
promise 的原理很簡單, 只要沒呼叫  resolve( ) 去解析, 就會維持在暫停(suspend)狀態, 一旦執行過 resolve( ) 後就變成滿足(fullfill)狀態. 後續當呼叫 then { } 區塊時, 若還是在暫停狀態下,沒意外(exception)的話,就維持該狀態,但要將該區塊(指標變數)附加(累積)到 promise 物件裏面,一旦呼叫了 resolve(result), resolve 除了會解析結果外, 同時還會就將結果傳遞給之前所累積的 then { } 區塊去執行. 也就是說只有在滿足狀態當下, promise 的 then { } 區塊才會執行. 有個應用情境是:將 promise.then { } 區塊放在函式內執行, 再用像是 window.setTimeout 或是 window.window.requestAnimationFrame 定時啟動該函式, 重複啟動函式時, 若 promise 一直維持在暫停的狀態, 就會造成 then 區塊不停的累積, 一旦狀態滿足時, 就有可能造成區塊重複執行的後遺症, 例如:
     import kotlin.js.Promise
     import kotlin.browser.*
     fun main() {   
           lateinit var resolve: (Int)->Unit
           val promise = Promise < Int > { Yes, _ ->
                resolve = Yes
           }
           var count = 0
           fun render( ) {
                 val capture = count
                 promise.then { result ->  // dupcate then if not fullfill
                       println("fullfill:  result=${result} capture=${capture}")
                 }
                 if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
           }
           window.requestAnimationFrame({render() })
           // promise not resolve
           window.setTimeout( {resolve(0)}, 2000)  // resolve result sometime or somewhere
     }
執行結果:
fullfill:  result=0 capture=0
fullfill:  result=0 capture=1
fullfill:  result=0 capture=2
fullfill:  result=0 capture=3
fullfill:  result=0 capture=4
fullfill:  result=0 capture=5
fullfill:  result=0 capture=6
fullfill:  result=0 capture=7
fullfill:  result=0 capture=8
fullfill:  result=0 capture=9
fullfill:  result=0 capture=10
為了用 kotlin 實現 async/await 的邏輯.  運用 promise 物件, 基本上就可以成為 async function, await 只能放在 async function 裏面, 直到 promise 完成解析(滿足狀態)才能進行後續動作, 因此特別用高階函式及 skip re-entry 技巧, 將 promise 包進新的類型, 並把他的 then 區塊打包放進高階函式當成 callback 函式去執行, 就能解決重複執行的困擾:
     import kotlin.js.Promise
     import kotlin.browser.*
     class myAsync {             
               var once = 0
               lateinit var resolve:(Int)-> Unit
               val promise = Promise < Int > { Yes, _ ->
                      resolve = Yes
               }
               fun bwait(callback: (Int) -> Unit ) { //  high order function with callback function
                    promise.then { result ->
                         if (once ++ == 0)  callback(result ) // 只執行第一次的 then  區塊
                    }
               }
               fun await(callback: (Int) -> Unit ) { //  high order function with callback function
                    once ++
                    promise.then { result ->
                         if (-- once == 0)  callback(result ) // 只執行最後一次的 then 區塊
                    }
               }
     }
     fun main() {
           val async = myAsync( )
           var count = 0
           fun render( ) {
                  val capture = count
                  async.await { result ->  // no duplicate then if fullfill
                       println("fullfill:  result=${result} capture=${capture}")
                 }
                 if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
           }
           window.requestAnimationFrame( { render() } )         
           window.setTimeout( {async.resolve(0)}, 2000) // resolve result sometime or somewhere
     }
若是用 async.await  等待執行的結果:
         fullfill:  result=0   capture=10
若是用 async.bwait  等待執行的結果:
         fullfill:  result=0   capture=0


2019年7月14日 星期日

for Kotlin

一直以來寫 c 或 Javascript 時習慣用:
               for (i = 0; i < 10; i++)  // ...
Kotlinfor 迴圈語法只能用 for(variable in Range) 來實現,像上述例子就可用單端式範圍遞增數列運算子 until 來實現:
               for (i in 0 until 10)     // until 是單端式範圍, 包含 0, 但不含 10
或是改成雙端式範圍遞增數列運算子 .. 加以實現:
               for(i in 0 .. 9 ) // .. 是雙端式範圍, 0 與 9 皆包含在內
累進數(等差級數步階值)大於 1, 就要加上 step,  像上式用 c 或 Javascipt寫的步階改為 2:
               for (i = 0; i < 10; i+=2)  // ...
在 Kotlin 就直接加上 step 2:
               for (i in 0 until 10 step 2) // ...
如果是遞減數列(數字大往數字小的等差級數), 像是 c 或 Javascript 所寫雙端式範圍的迴圈:
               for (i=10; i>=0; i-=2) // 10, 8, 6, ..., 0
因為在 Kotlin 語法中, 步階值必須是正整數, 雙端式範圍遞減數列用 downTo 關鍵字來實現:
               for (i in 10 downTo 0 step 2) // ...
參考文章:    https://kotlinlang.org/docs/tutorials/kotlin-for-py/loops.html

2019年7月5日 星期五

使用 kotlinc-js 編譯程式碼快速學習 Kotlin 語言

1. 到官網下載 kotlin compiler 編譯器(kotlin-compiler-1.3.40.zip), 用 unzip 解壓縮並放在適當目錄(例如 pathToKotlinc), 編譯的執行檔會在目錄 bin 底下, 程式庫在目錄 lib 底下. 再將 lib 目錄下的檔案 kotlin-stdlib-js.jar 用 unzip 解壓縮至適當位置, 只提取裡面的 kotlin.jskotlin.js.map (debug 時會用到)檔案就可以了, 將他複製到工作目錄, 例如:
        cd
        mkdir   work
        cd work
        unzip  pathToKotlinc/lib/kotlin-stdlib-js.jar
2. 在工作目錄用文字編輯器寫一個 kotlin 程式, 例如:
        geany my.kt &
3. 將以下作複數運算的範例程式存成檔案 my.kt
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
        val square:Double =  r*r + i*i   // |C|2
        val length:Double = sqrt(square) // abs(C) = |C|  = sqrt(|C|2)
        operator fun plus(a:C):C { // +
            r += a.r
            i += a.i
            return this
        }
        operator fun minus(a:C):C {  // -
            r -= a.r
            i -= a.i
            return this
        }
        operator fun times(a:C):C { // *
            val temp = a.r * r - a.i * i // real part
            i = a.i*r + a.r*i  // image part
            r = temp
            return this
        }
        operator fun div(a:C):C { //  ÷
            if (a.length > 0f) {
                val temp = (a.r*r + a.i * i) / a.square // real part
                i = (a.i*r - a.r*i) / a.square  // image part
                r = temp
            } else throw Exception("Divide by zero(length=0)")
            return this
        }
        operator fun invoke( ):C{ // dump self complex number
            val real   = floor(r*100+0.5).toFloat()/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
            val imag = floor(i*100+0.5).toFloat()/100 // 虛部只顯示到小數點 2 位, 4捨 5 入
            if (imag < 0) println("$real - ${-imag} i")  else  println("$real + $imag i")
            return this
        }

fun main(){
    try {
         val c = C(3.0, 3.0) / C(5.0, 2.0)   //  c = (3 + 3i) / (5 + 2i)
          c( )  // dump
    } catch (e:Exception){
        println(e.message)
    }
}
4. 用 kotlin 的編譯器 kotlinc-js 將上述 kotlin 原始碼翻譯為 Javascript:
        pathToKotlinc/bin/kotlinc-js   my.kt   -output   my.js
或是利用 geany 的設定組建命令, 直接點選工具欄:  組建(B) - > 設定組件命令(S), 第一欄隨意輸入識別名稱(例如 kotlin2js),  第二欄輸入 kotlin2js 編譯器的位置及參數(%f 是目前編輯檔案名稱, %e 是不含副檔名的檔案名稱, -output 可以指定輸出的檔案名稱), 例如:
          pathToKotlinc/bin/kotlinc-js   %f   -output  %e.js
意思是說用  kotlinc-js 將目前的檔案翻譯成 Javascript, 輸出檔與編譯的同名稱但副檔名改為 js, 第三欄可以空白預設是目前的工作目錄, 設定好後, 用滑鼠點選: 組建 ->  kotlin2js 執行看看, 應該會在工作目錄編譯出 my.js.
若要在 vscode 裏面編譯, 參考  https://code.visualstudio.com/docs/editor/variables-reference, 編寫一個 tasks.json 檔案, 點選 Terminal -> Configure Tasks帶出編輯視窗, 把以下內容寫入:
{
    "tasks": [{
            "label": "Kotlinc2js",
            "type": "shell",
            "command": "pathToKotlinc/bin/kotlinc-js",
            "args": ["${file}", "-output", "${fileBasenameNoExtension}.js"],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }]
}
存檔後, 點選 Terminal -> Run Build task 或是用快速按鍵 Ctrl-Shift-B, 就能將目前編輯的 Kotlin 程式檔轉換成 Javascrip
5. 在工作目錄底下用文字編輯器另外編輯一個 my.html 準備讓瀏覽器開啟, 並在開發工具底下執行用 kotlin  語言所寫的程式碼(實際上是翻譯成兩個 Javascript 檔案: kotlin.js 及 my.js). 將以下內容存檔(my.html):
<html><head><meta charset="UTF-8"></head>
<body>
        <script type="text/javascript" src="kotlin.js"></script>
        <script type="text/javascript" src="my.js"></script>
</body>
</html>
6. 用瀏覽器 chrome 或是 firefox 打開上述 my.html 檔案, 接著按滑鼠右鍵點選檢查原始碼來開啟內建的開發工具, 在開發工具欄點選 console 就能看到程式的輸出結果

Kotlin 的 Delegated Properties (被代理的變數或特性)

參考文章:  https://kotlinlang.org/docs/reference/delegated-properties.html
針對類型裡的特性(propertys), 常數(val)的特性內定 getter 是用 get( ) 方法去讀取主角(field)的值, 對於可寫入的特性(var)而言, 內定 setter 則用 set(v: Type) 方法設定幕後的主角(field), 除非要另外處理 field 的運算才需改寫, 否則 kotlin 會自動加上必要程式碼:
      class SomeClass {
            var   v: String = "?"
                    //  get( ) { return field }
                    //  set(s:String) { field = s }
            val   readonly: Int = 0
                    //  get( ) { return field }
      }
物件在引用特性時不必後綴成對小括號, 因此很簡潔. 物件引用方法( ) 時可以看做是一個運算的結果, 也就是所謂的函數. Kotlin 針對特性或變數有個特別條款可以用 by 跑龍套, 也就是說留待跑龍套代為處置, Koltin  已經為跑龍套主角(object)指定了兩個運算 operator getValue( )  及 operator setValue( ) 來處理特性或變數的讀寫機制, 若想改寫,可以 import pacage 針對讀與寫操作分別作運算,其中對應 get( ) 方法就是 operator getValue( ), 對應  set( ) 方法就是 operator setValue( ), 詳如範例:
import kotlin.reflect.*
class InitString (str: String) { // 代理的主角類型
    var bakendField: String = str  // 真正被讀寫的幕後主角
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
         print(property.name + " : ")
         return bakendField  // return value for method get( ), 讀取
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, v: String) {
        println(property.name + " = " + v)
        bakendField = v     // set value to property for method set(v), 寫入
    }
}
fun main(){
    var c : String   by   InitString("???")  // 用物件 InitString( ) 代理改寫變數的讀/寫機制
    println(c)  // 讀取變數 c
    c = "dd"    // 設定變數 c
    println(c)  // 重新讀取變數 c
}
執行結果:
c : ???
c = dd
c : dd

2019年7月4日 星期四

Kotlin 的 data class 搭配有趣的運算子 operator fun

利用 data class 簡單就能定義複數(實部及虛部), 再定義出類型的運算方法:  operator fun plus( ), minus( ), times( ), div( ) 這四則運算, 就能用來計算複數, 若定義出 operator fun involke( ) 這個方法就能讓物件身兼特定功能(c++ 稱為 functor):
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
        val square:Double =  r*r + i*i   // |C|2
        val length:Double = sqrt(square) // abs(C) = |C|  = sqrt(|C|2)
        operator fun plus(a:C):C { // +
            r += a.r
            i += a.i
            return this
        }
        operator fun minus(a:C):C {  // -
            r -= a.r
            i -= a.i
            return this
        }
        operator fun times(a:C):C { // *
            val temp = a.r * r - a.i * i // real part
            i = a.i*r + a.r*i  // image part
            r = temp
            return this
        }
        operator fun div(a:C):C { //  ÷
            if (a.length > 0f) {
                val temp = (a.r*r + a.i * i) / a.square // real part
                i = (a.i*r - a.r*i) / a.square  // image part
                r = temp
            } else throw Exception("Divide by zero(length=0)")
            return this
        }
        operator fun invoke( ):C{ // dump self complex number           
            val real = floor(r*100+0.5)/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
            val imag = floor(i*100+0.5)/100 // 需部只顯示到小數點 2 位, 4 捨 5 入
            if (imag < 0) println("$real - ${-imag} i")  else  println("$real + $imag i")
            return this
        }
}   
fun main(){
    try {
         val c = ( ( C(3.0, 3.0) + C(2.0, 1.0) ) * C(9.0, 2.0) - C(10.0, 2.0) ) / C(5.0, 2.0)
          c( )  // Complex number dump: c = (((3 + 3i)+(2 + i))*(9 + 2i)-(10 + 2i))÷(5 + 2i)
    } catch (e:Exception){
        println(e.message)
    }
}
輸出結果:
7.68 - 5.72 i

2019年7月2日 星期二

理解 Kotlin 的高階函式

參考文章: https://kotlinlang.org/docs/reference/lambdas.html
Kotlin 是一個 typesafe 程式語言, 每個變數都必須需確認他的資料型態(type), 函式的名稱可以看成是常數(變數), 因此函式也不例外需要加以宣告. 程式資料型態(function type)可以用成對小括號加上箭頭符號組成 ( ) -> , 用 typealias 定義函式型態後可以讓程式碼變得簡單易讀, 同時也能重複利用. 定義函式時若將函式當作輸入參數或是輸出函數,他就是所謂的高階函式 :
typealias  funit =  ( ) -> Unit   // 函式資料型態: 無輸入參數, 也不傳回任何數值
fun g(f: funit) { // f 是一個輸入函式, 因此 g(f) 是高階函式,  f 可以在函式 g 裏面執行
    println("Run in HighOrderFunction")
    f( ) // 輸入的函式在此開始執行
}
fun main() {
    val  f: funit =  {  println("Hello")        }  // f 指定成 λ 函式, 符合"無輸入參數,無傳回值"
    g( f ) // 把函式丟進高階函式裏面去執行,
}
有一種有趣的高階函式:輸入參數是一個攜帶著物件類型的函式(lambda with Receiver), 透過綁定物件的手法, 讓呼叫的方法就好像侷限(scope)在物件裏面執行(context)一樣:
        class Classname( ) {
              fun doMethod( ) {
                    println("This is a method")
              }
        }
        fun HighOrderλwithReceiver(λ: Classname.( ) -> Unit) {
             val objectContext =  Classname( )            
             objectContext.λ ( )
        } 
        fun main( ) { 
               // fun doMethod { println("Global") }
                HighOrderλwithReceiver  {       
                        // fun doMethod { println("Local") }                   
                       doMethod( )   // this.doMethod( )
                }
        }
上述當呼叫 HighOrderλwithReceiver 函式時, 因為最後一項參數是 λ 函式, 所以將它搬離出小括號, 搬離後剛好沒有參數, 小括號也跟著不用寫, 這是 Kotlin 語言的特異功能. 傳進來的 λ 區塊 {   } , 在裏面的變數若沒有指定物件的範疇(scope)時, 編譯器就會在當地區域(Local scope),物件的本體區域(context 也就是 this), 及整體區域(Global scope) 推測(infer)並編譯(compile)出適當的變數或函數, 因為在高階函式 HighOrderλwithReceiver 的定義裏面, 已經預先設定進來的函式屬於 Classname 類型的一種方法, 就好像是先築起一道牆(scope), 利用類型建立起物件副本(instance)也就是物件文本(context),接著在物件的範疇下執行 λ 裏面的每一道程序, 因此 λ 區塊裏面, 呼叫的函式若是 Classname 裡面的從屬函數的話, this 就可以省略不寫了. 這是 Function literals with receiver 厲害的地方.

2019年7月1日 星期一

Kotin 的 Coroutines Channels

Coroutine 之間的訊息溝通可以透過 Channel(通道), 只要在程式開頭處 import package:
           import kotlinx.coroutines.channels.*
之後才能使用 Kotlin 所提供的函式庫. 參考文章:
https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/08_Channels
通道是一種物件類型, 他所提供的方法 send 和 receive 是一種會依據狀況而隨時暫停(suspend)或重啟(resume)執行的函式(suspend function), 因此也只能用在 coroutine 區塊內發動. 接收通道的介面是 ReceiveChannel, 發送通道的介面是 SendChannel, 兼具接收與發送雙向類型的通道就是 Channel, 根據通道內容物的數量又可以區分幾種型式:
1. 數量為 0 時, 也就是無緩衝, 稱為 Rendezvous buffer(不見不散型通道), 收發端發現沒有東西就會在那邊等(suspend), 直到對方出現後才各自分道揚鑣(resume), 這是預設的通道型式:
      val 不見不散通道 = Channel < String > ( )
2. 大於 0 且數量有限, 稱為 Buffer channel (緩衝型通道), 一旦緩衝區滿了,發送端才會暫停(suspend), 而接收端只要發現沒東西時就會在那邊等(suspend).狀況一旦解除就會重啟(resume):
      val 緩衝通道 = Channel < String > (10)
3. 緩衝數量是 1, 且發送端不受限, 因此收發端只會收到最後一筆(前面發送的資料會被覆蓋)稱為 CONFLATED buffer (合而為一型緩衝通道),只有當接收端沒看到東西才會等待:
      val 合而為一通道 = Channel < String > (CONFLATED)
4. 數量無限制時就是 UNLIMITED buffer(超級緩衝通道), 它就像是一個先進先出有秩序的隊伍(Queue), 只有當記憶體不夠時, 發送端才會產生例外(exception), 接收端只要收不到東西時就在那邊等待(suspend),狀況一旦解除就會重啟(resume):
      val 超級通道 = Channel < String > (UNLIMITED)