2019年3月25日

Kotlin Lambda

kotlin standard library 大量使用了 lambda 語法,最常見的就是用在 collections,另外也提供如何從 java 呼叫 kotlin lambda 的方式。最後說明一種特別的 lambda with receivers。

lambda expression

直到 Java 8 才提供了 lambda 語法。以下說明 lambda 的重要性

block of code as method parameters

如果要實作當某個事件發生時,執行一個 handler 的 code,以 java 來說可以用 anonymous inner class 實作。如果用 functional programming 的方式,可以將 function 當作 value,也就是將 function 當作參數傳遞給另一個 function,也就是把一小段 code 當作method 參數。

listener 實作 OnclickListener 介面,覆寫 onClick

/* Java */
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        /* actions on click */
    }
});

換成functional progeamming 的方式

button.setOnClickListener { /* actions on click */ }
lambda and collections

假設有個 list of data class: Person,需要找到裡面年紀最大的人,直覺會以 function 實作

data class Person(val name: String, val age: Int)

fun findTheOldest(people: List<Person>) {
    var maxAge = 0
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(theOldest)
}

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    findTheOldest(people)
}

kotlin 有提供這個 libray function: maxBy,maxBy 可用在任何一種 collection,後面的 {} 是 lambda implementation

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age })
Person(name=Bob, age=31)

maxBy 也可以直接指定 member reference

people.maxBy(Person::age)
lambda expression 語法

lambda 語法是在 {} 裡面,前面是參數,後面是 body

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))
// 3

也可以直接呼叫 lambda

>>> { println(42) }()
42

但最好用 run 封裝起來,code 會比較容易閱讀

>>> run { println(42) }
42

剛剛的 people.maxBy { it.age } 其實原本應該寫成,使用 p:Person 這個參數,運算 p.age

people.maxBy({ p: Person -> p.age })

可以將 () 省略

people.maxBy { p: Person -> p.age }

將 p 的資料型別省略

people.maxBy { p -> p.age }

將 p 改成 lambda 的預設參數名稱 it

people.maxBy { it.age }

joinToString 也有在 kotlin 標準函式庫裡面,其中 transform 就是需要提供一個 lambda function

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> val names = people.joinToString(separator = " ", transform = { p: Person -> p.name } )
>>> println(names)
Alice Bob

也可以簡化寫成這樣

people.joinToString(" ") { p: Person -> p.name }

lambda 語法裡面,也可以寫超過一個 expression 或 statement

val sum = { x: Int, y: Int ->
    println("Computing the sum of $x and $y")
    x + y
}
accessing variables

如果在 method 裡面宣告 anonymous inner class 時,可使用 method 裡面的 local 變數以及 parameters,lambda 也是一樣。

messages.forEach 裡面的 lambda function 使用了外部 method 的參數 $prefix

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it")
    }
}

fun main(args: Array<String>) {
    val errors = listOf("403 Forbidden", "404 Not Found")
    printMessagesWithPrefix(errors, "Error:")
}

kotlin 跟 java 最大的差異是 kotlin 沒有限制只能使用 final variables,也可以在 lambda function 裡面修改變數的值。

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

fun main(args: Array<String>) {
    val responses = listOf("200 OK", "418 I'm a teapot",
                           "500 Internal Server Error")
    printProblemCounts(responses)
}
member references

如果 lambda 要傳送一個已經定義好的 function 時,要使用 :: operator,將 function 轉換為 value,這種語法稱為 member reference,前面是 class,後面是 method 或是 property

val getAge = Person::age

等同

val getAge = { person: Person -> person.age }

如果是 top-level function,就不用寫 class name

fun salute() = println("Salute!")

fun main(args: Array<String>) {
    run(::salute)
}

將 lambda function 轉送給 sendEmail function

val action = { person: Person, message: String ->
    sendEmail(person, message)
}

直接使用 member reference

val nextAction = ::sendEmail

也可以將 member reference 套用在 class constructor

>>> data class Person(val name: String, val age: Int)
>>> val createPerson = ::Person
>>> val p = createPerson("Alice", 29)
>>> println(p)
Person("Alice", 29)

也可以使用 extension function

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult

collection 使用的 functional APIs

filter and map

取得偶數的元素,it 是 lambda 的預設變數名稱

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3, 4)
    println(list.filter { it % 2 == 0 })
}

取得年齡超過30歲的 Person

data class Person(val name: String, val age: Int)

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.filter { it.age > 30 })
}

取得每一個元素的平方 list

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3, 4)
    println(list.map { it * it })
}

取得所有人的名字 list

data class Person(val name: String, val age: Int)

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.map { it.name })
}

也可以寫成people.map(Person::name)

可以將 filter 與 map 連在一起 people.filter { it.age > 30 }.map(Person::name)

找到年齡最大的 personpeople.filter { it.age == people.maxBy(Person::age).age } 但因為 people.maxBy(Person::age).age 重複做了很多次,所以將這兩個分開做,效能會比較好

val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }

使用 filter 及 transformation functions to maps

fun main(args: Array<String>) {
    val numbers = mapOf(0 to "zero", 1 to "one")
    println(numbers.mapValues { it.value.toUpperCase() })
}

//{0=ZERO, 1=ONE}
all, any, count, find

檢查是不是所有元素都符合某個條件

data class Person(val name: String, val age: Int)

val canBeInClub27 = { p: Person -> p.age <= 27 }

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 27), Person("Bob", 31))
    println( people.all(canBeInClub27) )
}

檢查是否至少有一個元素符合某個條件

println(people.any(canBeInClub27))

!allany 的意義是相反的,可以互換

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(!list.all { it == 3 })
    println(list.any { it != 3 })
}

count 計算數量

al people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.count(canBeInClub27))

用 find 找到一個符合條件的元素

data class Person(val name: String, val age: Int)

val canBeInClub27 = { p: Person -> p.age <= 27 }

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 27), Person("Bob", 31))
    println(people.find(canBeInClub27))
}
groupBy: 將 list 轉換為 map of groups

將 list 以 age 分組

data class Person(val name: String, val age: Int)

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 31),
            Person("Bob", 29), Person("Carol", 31))
    println(people.groupBy { it.age })
}

執行結果為 Map<Int, List<Person>>

{31=[Person(name=Alice, age=31), Person(name=Carol, age=31)], 29=[Person(name=Bob, age=29)]}

以 string 的第一個 character 分組

fun main(args: Array<String>) {
    val list = listOf("a", "ab", "b")
    println(list.groupBy(String::first))
}

執行結果

{a=[a, ab], b=[b]}
flatMap, flatten

flatMap 做兩件事:根據參數,轉換 (maps) 每一個 element 為 collection,合併 (flattens) 數個 lists 為一個

fun main(args: Array<String>) {
    val strings = listOf("abc", "def")
    println(strings.flatMap { it.toList() })
}

執行結果

[a, b, c, d, e, f]

set of all authors

class Book(val title: String, val authors: List<String>)

fun main(args: Array<String>) {
    val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
                       Book("Mort", listOf("Terry Pratchett")),
                       Book("Good Omens", listOf("Terry Pratchett",
                                                 "Neil Gaiman")))
    println(books.flatMap { it.authors }.toSet())
}

執行結果

[Jasper Fforde, Terry Pratchett, Neil Gaiman]

lazy collection operations: sequences

sequence 不同於 collection,在使用到該物件時才會進行運算,而不是在定義時,就馬上進行運算,也不會產生 intermediate temporary objects

asSequence 是讓 collection 轉成 sequence,toList 則是讓 sequence 轉回 collection

fun main(args: Array<String>) {
    val seq = listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it) "); it * it }
            .filter { print("filter($it) "); it % 2 == 0 }

    println( "after seq")
    val list = seq.toList()

    println( )
    println( list )
}

執行結果

after seq
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16) 
[4, 16]

在 sequence 的 operations 中,map 及 filter 是 intermediate operations,toList 是 terminal operation,一直到 terminal operation 才會進行運算

sequence.map{...}.filter{...}.toList()

除了 asSequence() 還可以用 generateSequence() 產生 sequence,一直到呼叫 sum 的時候,才會進行運算

fun main(args: Array<String>) {
    val naturalNumbers = generateSequence(0) { it + 1 }
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
    println(numbersTo100.sum())
}

另一個常用的是 sequence of parents,以下是檢查 file 是否在某一個隱藏的目錄中,所以要產生 sequence of parent directories,然後檢查是否有任一個為 hidden

import java.io.File

fun File.isInsideHiddenDirectory() =
        generateSequence(this) { it.parentFile }.any { it.isHidden }

fun main(args: Array<String>) {
    val file = File("/Users/svtk/.HiddenDir/a.txt")
    println(file.isInsideHiddenDirectory())
}

using Java functional interfaces

Kotlin lambdas 可跟 Java APIs 一起使用

在 java 的 setOnClickListener

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        ...
    }
}

在 kotlin

button.setOnClickListener { view -> ... }

其中 OnClickListener 稱為 funcational interface 或是 SAM(single abstract method) interfaces

public interface OnClickListener {
    void onClick(View v);
}
passing a lambda as a parameter to a java method

將 lambda 傳到需要 functional interface 的 java method

/* Java */
void postponeComputation(int delay, Runnable computation);

在kotlin 可這樣呼叫,compiler 會自動產生一個 instance of Runnable,也就是 instance of an anonymous class implements Runnable

postponeComputation(1000) { println(42) }

compiler 會用 run method 產生 instance of anonymous class that implements Runnable

postponeComputation(1000, object: Runnable {
    override fun run() {
        println(42)
    }
})

每一個呼叫 handleComputation 都會產生一個新的 Runnable instance 儲存 id 欄位

fun handleComputation(id: String) {
    postponeComputation(1000) {
        println(id)
    }
}
SAM constructors: explicit conversion of lambdas to functional interfaces

SAM constructor 是 compiler 產生的 function,可轉換 lambda 為 instance of functional interface

我們不能直接 return lambda,但可包裝在 SAM constructor 裡面

fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All done!") }
}

fun main(args: Array<String>) {
    createAllDoneRunnable().run()
}

例如 Android 的 listener,可以產生出來給多個 button 使用

val listener = OnClickListener { view ->
    val text = when (view.id) {
        R.id.button1 -> "First button"
        R.id.button2 -> "Second button"
        else -> "Unknown button"
    }
    toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)

lambda with receivers: with and apply

以下這是 kotlin 的 lambda 的功能,不能在 java 使用。

可以在 lambda 呼叫 methods of a different object,不需要額外的 qualifiers

with

以下是列印英文字母的範例

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
         result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

fun main(args: Array<String>) {
    println(alphabet())
}

用 with 改寫

fun alphabet(): String {
    val stringBuilder = StringBuilder()
    // 指定 receiver value
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            // 以 this 呼叫 receiver value 的 method
            this.append(letter)
        }
        // 呼叫 method,但省略 this
        append("\nNow I know the alphabet!")
        // 由 lambda 回傳 value
        this.toString()
    }
}

fun main(args: Array<String>) {
    println(alphabet())
}

省略 this

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}

fun main(args: Array<String>) {
    println(alphabet())
}
apply

with 的 value 會回傳 lambda code 的最後一個 expression,但有時需要 return receiver object,而不是 lambda 運算結果,這時要改用 apply

apply 用起來就像是 with,差別是 apply 會回傳傳入作為參數的 object

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()

fun main(args: Array<String>) {
    println(alphabet())
}

也可以改用 buildString,buildString 會處理 StringBuffer,也會呼叫 toString,buildString 是 lambda with a receiver,且 receiver 是 StringBuilder

fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}

fun main(args: Array<String>) {
    println(alphabet())
}

References

Kotlin in Action

2019年3月18日

Kotlin 類別、物件、介面

以下說明這些功能:類別、介面,關鍵字 object,data classes 及 class delegation。

不同於 java,kotlin 宣告預設為 public final,nested class 不是 inner class,沒有外層 class 的 implicit reference。將 class 宣告為 data class時,compiler 會自動產生幾個 methods。

定義 class 時使用關鍵字 keyword,同時會產生一個 instance,這可用在 singleton object, companion objects 以及 oject expressions (類似 Java anonymous classes)

class hierarchy

interface

用 interface 定義介面,用 : 表示實作 interface

override 類似 java 的 @Override annotation,Button 裡面的 override 是一定要寫的,否則程式將無法編譯

interface Clickable {
    // 只定義了 function,沒有實作
    fun click()
}
class Button : Clickable {
    override fun click() = println("I was clicked")
}

fun main(args: Array<String>) {
    Button().click()
}

有兩個以上的 interface 的範例

class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")

    // 因 Clickable, Focusable 都有實作 showOff,必須要用 super<Clickable> 指定是用哪一個上層實作
    // 如果只要使用一種上層實作,可寫成這樣
    // override fun showOff() = super<Clickable>.showOff()
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

interface Clickable {
    fun click()
    // 定義 function,並提供預設的實作
    fun showOff() = println("I'm clickable!")
}

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.")

    fun showOff() = println("I'm focusable!")
}

fun main(args: Array<String>) {
    val button = Button()
    button.showOff()
    // Focusable 有提供 setFocus,故 Button 可使用這個 function
    button.setFocus(true)
    button.click()
}

執行結果

I'm clickable!
I'm focusable!
I got focus.
I was clicked
final by default

為了避免發生 fragile base class 問題,kotlin 預設將 class, method 都加上 final 的特性,如果要讓其他類別繼承的
class 或覆寫的 method,都必須要加上 open 這個 modifier。

interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}

// 可被繼承
open class RichButton : Clickable {

    // 不可被覆寫
    fun disable() {}

    // 可被覆寫
    open fun animate() {}

    // 覆寫 click,且可被覆寫
    override fun click() {}
    
    // 如果要限制 click 不能被覆寫,要加上 final
    // final override fun click() {}
}
class modifier corresponding member comments
final can't be overridden default for class members
open can be overridden 要特別指定
abstract must be overridden 只能用在 abstract classes, abstract members 不能有實作內容
override overrides a member in a superclass 預設為 open,如要限制不能被覆寫,要加上 final

visibility modifiers 有 public, protected, private, internal 四種,預設為 public,其中 internal 表示為 "visible inside a module"

visibility modifiers class member top-level declaration
public(default) visible everywhere visible everywhere
internal visible in a module visible in a module
protected visible in subclasses --
private visible in a clsss visible in a file

protected member 只能被 class 及 subclass 使用,跟 java 的 protected,可在同一個 package 使用的定義不同

class 的 extension function 無法使用 private or protected members

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.")

    fun showOff() = println("I'm focusable!")
}

internal open class TalkativeButton : Focusable {
    private fun yell() = println("Hey!")
    protected fun whisper() = println("Let's talk!")
}

// error: 'public' member exposes its 'internal' receiver type TalkativeButton
fun TalkativeButton.giveSpeech() {
    // error: cannot access 'yell': it is private in 'TalkativeButton'
    yell()
    // error: cannot access 'whisper': it is protected in 'TalkativeButton'
    whisper()
}
nested class, inner class

kotlin 也可在 class 裡面定義另一個 class,跟 java 的差異是 kotlin nested class 不能使用 outer class instance

首先製作兩個 interface,注意 State 為 Serializable

interface State: Serializable

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}

如果以 java 實作 ButtonState 這個 inner class,會遇到 java.io.NotSerializableException: Button,這是因為 ButtonState 裡面儲存了 outer class: Button 的 implicit reference,無法被序列化,如要解決,要將 ButtonState 宣告為 static,去掉 implicit reference to Button。

/* Java */
public class Button implements View {
    @Override
    public State getCurrentState() {
        return new ButtonState();
    }

    @Override
    public void restoreState(State state) { /*...*/ }
    
    public class ButtonState implements State { /*...*/ }
}

以 kotlin 實作 ButtonState,就像是 java 的 static nested class 一樣。

class Button : View {
    override fun getCurrentState(): State = ButtonState()

    override fun restoreState(state: State) { /*...*/ }

    class ButtonState : State { /*...*/ }
}

kotlin 的 nested class 不會參考到 outer class 的 reference,但 inner class 可以

Class A declared within another Class B in Java in Kotlin
nested class (不會儲存 outer class 的 reference) static class A class A
inner class (會儲存 outer class 的 reference) class A inner class A

可用 this@Outer 參考到 Outer class 的 reference

class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}
sealed class

Expr 介面有兩個實作的 class: Num, Sum,在 eval 中,要注意 else 的部分,compiler 會檢查有沒有預設的條件,在這邊預設不能回傳任何數值,故直接 throw exception

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

fun main(args: Array<String>) {
    println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}

kotlin 提供 sealed class 處理這種問題

因 Expr 限制為 sealed class,故無法被任意產生 subclass,所有 subclass 只能寫成 nested class,而在 eval 裡面,就不需要寫 else 的條件,因為系統內不可能會有其他 Expr 的 subclasses (compiler 的檢查及限制)

sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }

fun main(args: Array<String>) {
    println(eval(Expr.Sum(Expr.Sum(Expr.Num(1), Expr.Num(2)), Expr.Num(4))))
}

使用 nontrivial constructor 及 properties

kotlin 也可以實作多個 constructor,可在 initializer blocks 增加 initilization logic

primary constructor 及 initializer blocks

constructor, init 這兩個關鍵字,用 _nickname 取代 java 習慣的 this.nickname = nickname 這樣的寫法

// 單一參數的 primary constructor
class User constructor(_nickname: String) {
    val nickname: String
    
    // initializer block
    init {
        nickname = _nickname
    }
}

可省略 contructor, init 的語法

class User(_nickname: String) {
    val nickname = _nickname
}

再進一步省略 {} 的部分

class User(val nickname: String)

在 contructor 的參數,可給予預設值,使用時,就可彈性使用一或兩個參數的 constructor

class User(val nickname: String,
           val isSubscribed: Boolean = true)

fun main(args: Array<String>) {
    val alice = User("Alice")
    println(alice.isSubscribed)
    val bob = User("Bob", false)
    println(bob.isSubscribed)
    val carol = User("Carol", isSubscribed = false)
    println(carol.isSubscribed)
}

如果 super class 的 constructor 需要參數,要在 subclass 定義裡面提供該參數初始化 super class

open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

預設會產生一個沒有參數的 contructor

open class Button

class RadioButton: Button()

可讓 contructor 變成 private method

class Secretive private constructor() {}

class Secretive {
    private constructor()
}
secondary constructor
open class View {
    constructor(ctx: Context) {
        // some code
    }

    // secondary constructor
    constructor(ctx: Context, attr: AttributeSet) {
        // some code
    }
}

// subclass 必須同時提供兩個 constructor
class MyButton : View {
    constructor(ctx:Context) : super(ctx) {
        // ...
    }
    constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
        // ...
    }
}

可以用 this() 呼叫其他 constructor

class MyButton : View {
    constructor(ctx: Context): this(ctx, MY_STYLE) {
        // ...
    }
    constructor(ctx: Context, attr: AttributeSet):super(ctx, attr) {
        // ...
    }
}
實作 interfaces 中宣告的 properties

interface User 中定義需要有 nickname 這個 property,PrivateUser、SubscribingUser、FacebookUser 分別用三種方式,提供 nickname

interface User {
    val nickname: String
}

// primary constructor property
class PrivateUser(override val nickname: String) : User

// custom getter
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

// property initializer
fun getFacebookName(accountId: Int) = "fb:$accountId"
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)
}

fun main(args: Array<String>) {
    println(PrivateUser("test@kotlinlang.org").nickname)
    println(SubscribingUser("test@kotlinlang.org").nickname)
}

interface 可讓 property 有 getter setter 實作

interface User {
    val email: String
    val nickname: String
        get() = email.substringBefore('@')
}
由 getter/setter 使用 backing field

field 可在 getter/setter 中取得該 backing field property 的 value,在 setter 裡面,可修改 field 的 value

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""
                Address was changed for $name:
                "$field" -> "$value".""".trimIndent())
            field = value
        }
}

fun main(args: Array<String>) {
    val user = User("Alice")
    user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
}
修改 accessor visibility

counter 的 set 改成 private method,因此,不能在 class 以外的地方,修改 counter 的數值

class LengthCounter {
    var counter: Int = 0
        private set

    fun addWord(word: String) {
        counter += word.length
    }
}

fun main(args: Array<String>) {
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("Hi!")
    println(lengthCounter.counter)
}

compiler 產生的 methods: data classes 及 class delegation

java 在每一個 class 都有 equals(), hashCode(), toString() 這些 method,kotlin 在 compiler 中自己產生這些 method

universal object methods
  • toString()

override toString() method

class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

fun main(args: Array<String>) {
    val client1 = Client("Alice", 342562)
    println(client1)
}
  • equals()

沒有調整 equals() 時,兩個物件的比較結果是相異的

class Client(val name: String, val postalCode: Int)

fun main(args: Array<String>) {
    val client1 = Client("Alice", 342562)
    val client2 = Client("Alice", 342562)
    println(client1 == client2)
}

覆寫 equals(),Any? 其中 Any 代表 java.lang.Object ,這也是 Kotlin 所有 classes 的 super class,? 代表 other 可以是 null

hashCode 必須要跟著 equals 一同被覆寫,因為當兩個 object 相等時,hashCode 必須要一樣

class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?): Boolean {
        // 先檢查 other 是不是 null, 是不是 Client
        if (other == null || other !is Client)
            return false
        // 檢查兩個欄位的數值是否相等
        return name == other.name &&
               postalCode == other.postalCode
    }
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
    
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

fun main(args: Array<String>) {
    val processed = hashSetOf(Client("Alice", 342562))
    println(processed.contains(Client("Alice", 342562)))
}
data classes: 自動產生 universal methods 的實作

在 class 前面加上 data 關鍵字,compiler 將會自動產生 universal methods: equals(), hashCode(), toString()

data class 並沒有限制只能使用 val property,也可以用 var,但建議還是盡量使用 val,讓 data class 成為 immutable object,所以 kotlin 在提供了一個 copy() 這個 universal method

如果自己實作 copy,會類似以下這樣的 code

class Client(val name: String, val postalCode: Int) {
    ...
    fun copy(name: String = this.name, postalCode: Int = this.postalCode) = Client(name, postalCode)
}

object instance 使用 copy,並修改其中一個欄位的值

>>> val bob = Client("Bob", 973293)
>>> println(bob.copy(postalCode = 382555))
Client(name=Bob, postalCode=382555)
class delegation: 使用 "by"

在物件導向程式中,會因為繼承 class,覆寫 method,讓某些程式跟父類別產生連動關係,當 base class 有異動時,就造成整個系統不穩定的狀況。

kotlin 預設讓 class 都是 final,無法被繼承的,這可確保經過設計,可以被繼承的 class 才有被繼承的功能,

相較於繼承,更常見的方式,是利用 Decorator Pattern,建立一個新的 class,實作跟舊的 class 一樣的所有 methods,再根據自己的需求,修改對應的 methods,就像是以下的範例。

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()
    
    override val size: Int get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
}

kotlin 提供另一種更有效的方式: class delegation 使用 "by",以下這樣的 code,compiler 會自動產生所有 delegation codes,只需要寫有需要修改覆寫的 method。

class DelegatingCollection<T>(
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {
}
import java.util.HashSet

class CountingSet<T>(
        val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {

    var objectsAdded = 0

    // 不使用預設的 delegation method,改寫 add
    override fun add(element: T): Boolean {
        objectsAdded++
        return innerSet.add(element)
    }

    override fun addAll(c: Collection<T>): Boolean {
        objectsAdded += c.size
        return innerSet.addAll(c)
    }
}

fun main(args: Array<String>) {
    val cset = CountingSet<Int>()
    cset.addAll(listOf(1, 1, 2))
    println("${cset.objectsAdded} objects were added, ${cset.size} remain")
}

關鍵字 object 的用途

  • Object declaration: 定義 singleton
  • Companion objects: 包含 factory method 以及所有不需要 instance 的 methods,透過 class name 使用這些 methods
  • Object expression: 取代 Java 的 anonymous inner class
singleton

object 可讓該 class 只會產生一個 instance,使用時必須要用 class name

object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}

Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()

以下是 CaseInsensitiveFileComparator 範例

import java.util.Comparator
import java.io.File

object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(file1: File, file2: File): Int {
        return file1.path.compareTo(file2.path,
                ignoreCase = true)
    }
}

fun main(args: Array<String>) {
    println(CaseInsensitiveFileComparator.compare(
        File("/User"), File("/user")))
    
    val files = listOf(File("/Z"), File("/a"))
    println(files.sortedWith(CaseInsensitiveFileComparator))
}

也可以將 object 放在 class 裡面,以 Person.NameComparator 的方式使用

import java.util.Comparator

data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int =
            p1.name.compareTo(p2.name)
    }
}

fun main(args: Array<String>) {
    val persons = listOf(Person("Bob"), Person("Alice"))
    println(persons.sortedWith(Person.NameComparator))
}

如果要從 Java 使用 kotlin object,要透過 INSTANCE 這個 singleton object reference

/* Java */
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);
companion objects: factory method and static members

java 的 keyword static,在 kotlin 沒有。

kotlin 提供 companion object,使用起來就像是java 的 static method 一樣

class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}

fun main(args: Array<String>) {
    A.bar()
}

通常會針對 FacebookUser, SubscribingUser 兩種 User,實作不同的 constructor

class User {
    val nickname: String
    constructor(email: String) {
        nickname = email.substringBefore('@')
    }

    constructor(facebookAccountId: Int) {
        nickname = getFacebookName(facebookAccountId)
    }
}

如果用 companion object 的做法,可在裡面實作 factory method,FacebookUser, SubscribingUser 兩種不同的 User 就用不同的 factory method 產生物件

fun getFacebookName(accountId: Int) = "fb:$accountId"

class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}

fun main(args: Array<String>) {
    val subscribingUser = User.newSubscribingUser("bob@gmail.com")
    val facebookUser = User.newFacebookUser(4)
    println(subscribingUser.nickname)
}
把 companion object 當作 regular objects

companion object 就像是在 class 中宣告的一般類別,可命名,可實作 interface,可以有 extension function 或 properties

  • json serialization
class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person = ...
    }
}

fun main(args: Array<String>) {
    val person = Person.Loader.fromJSON("{name: 'Dmitry'}")
    println(person.name)
    val person2 = Person.fromJSON("{name: 'Brent'}")
    println(person2.name)
}
  • implement interface
interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person = ...
    }
}

在 java 如果要使用 companion object

/* Java */
Person.Companion.fromJSON("...");
  • companion object extensions
// business logic module
class Person(val firstName: String, val lastName: String) {
    // 產生空白的 companion object
    companion object {
    }
}
// client/server communication module
// 產生 extension function
fun Person.Companion.fromJSON(json: String): Person {
    ...
}

val p = Person.fromJSON(json)
  • anonymous inner class

在 java 使用 event listener,會用到 anonymous inner class

window.addMouseListener(
    object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            // ...
        }
        override fun mouseEntered(e: MouseEvent) {
            // ...
        }
    }
)

在 java 的 anonymous class 中,只能使用外部的 final variables。

在 kotlin 可以建立 local 變數,並在 object 裡面使用該變數

fun countClicks(window: Window) {
    // 宣告 local 變數
    var clickCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            // 更新變數的數值
            clickCount++
        }
    })
    // ...
}

References

Kotlin in Action

2019年3月11日

Kotlin 宣告與呼叫函數

以下說明這些功能:collections, strings 及 regular expressions 的相關函數。以命名的參數及預設值,定義參數。透過擴增的 functions 及 properties,使用 Java Libraries。以 top-level 及 local function 的方式重組程式碼。

建立 collections

kotlin 的 .javaClass 就等同 java 的 getClass()

//println(set.javaClass)
//class java.util.HashSet
val set = hashSetOf(1, 7, 53)

//println(list.javaClass)
//class java.util.ArrayList
val list = arrayListOf(1, 7, 53)

//println(map.javaClass)
//class java.util.HashMap
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

kotlin 沒有建立自己的 collection classes 而是沿用 java 的 collection classes

// last() 取得最後一個 element
val strings = listOf("first", "second", "fourteenth")
println(strings.last())

// max() 取得最大值
val numbers = setOf(1, 14, 2)
println(numbers.max())
// println 會呼叫 list 的 toString()
val list = listOf(1, 2, 3)
println(list)

joinToString 將 collection 以 prefix, separator, postfix 的格式列印出來

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {

    val result = StringBuilder(prefix)

    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list, "; ", "(", ")"))
}

執行結果

(1; 2; 3)

為了避免每一次都需要填寫四個參數的麻煩,可利用以下方式處理

  • Named Arguments 呼叫 function 時,將參數命名
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
  • Default parameter values 函數的參數提供預設值
fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String

接下來可用以下方式呼叫函數

joinToString(list, ", ", "", "")
joinToString(list)
joinToString(list, "; ")
joinToString(list, prefix = "# ")

top-level functions and properties

取代 static utility clsses

建立 join.kt,放入以下 code

package strings

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {

    val result = StringBuilder(prefix)

    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

$ kotlinc join.kt 編譯後,會產生 strings/JoinKt.class

所有 top-level function 都會成為該 class 的 static methods

如果加上 @file:JvmName("StringFunctions") 就可改變 class 的名稱,為 strings.StringFunctions

@file:JvmName("StringFunctions")

package strings
fun joinToString(...): String { ... }

top-level properties 轉換到 java 時,一樣會產生 accessor methods, val 變數會產生 getter,var 變數產生 getter/setter。

const val UNIX_LINE_SEPARATOR = "\n" 加上 const,就會轉換為 java 的 public static final String UNIX_LINE_SEPARATOR = "\n";

extension functions and properties,將 method 增加到另一個 class 裡面

因 kotlin 通常會增加到某一個既有的 Java project 裡面,故須要將 kotlin 跟既有的 java code 整合在一起。可使用 extension function,在 class 外面訂製一個新的 function

package strings

fun String.lastChar(): Char = this.get(this.length - 1)

String 稱為 receiver type,也就是要擴充的 class 名稱,this 稱為 receiver object

以下的例子,String 為 receiver type,"Kotlin" 為 receiver object

>>> println("Kotlin".lastChar())
n

也可以將 this 省略

package strings

fun String.lastChar(): Char = get(length - 1)

extension functions 必須要另外 import 才能使用

import strings.lastChar
// import strings.*

val c = "Kotlin".lastChar()

也可以在 import 後,用 as 產生別名,這可以解決 conflict 問題

import strings.lastChar as last
val c = "Kotlin".last()

從 java 呼叫 extension functions

如果剛剛的 code 是放在 StringUtil.kt 裡面

/* Java */
char c = StringUtilKt.lastChar("Java");

以 utility class 作為 extensions

將 joinToString 擴充到 Collection 裡面

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(list.joinToString(separator = "; ",
          prefix = "(", postfix = ")"))
}

改成這樣,就只接受 String 的 collection

fun Collection<String>.join(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
) = joinToString(separator, prefix, postfix)

extension functions 無法被 overriding

在 class 裡面定義的 function 可以被 overriding

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() = println("Button clicked")
}

fun main(args: Array<String>) {
    val view: View = Button()
    view.click()
}

執行結果

Button clicked

extension functions 無法被 overriding,因為 extension function 是 static method

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() = println("Button clicked")
}

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

fun main(args: Array<String>) {
    val view: View = Button()
    view.showOff()
}

執行結果

I'm a view!

extension properties

extension property 就像是 property 再加上 receiver type,壹定要定義 getter,不能夠定義初始值,在 StringBuilder.lastChar 是定義為 var,因為 StringBuilder 裡面存的資料是會變動的。

val String.lastChar: Char
    get() = get(length - 1)

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

fun main(args: Array<String>) {
    // 使用起來就像是 property
    println("Kotlin".lastChar)
    
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!'
    println(sb)
}

其他支援 collections 的功能

Kotlin standard library 其他支援 collection 的功能

  • 關鍵字 vararg: 可定義有任意參數數量的 function
  • infix calls
  • destructuring declarations,可 unpack composite value 為 multiple variables

為什麼 Kotlin 跟 Java 使用相同的 collection class 但是卻有更多功能?原因是 last, max function 都是以 extension function 實作的。

>>> val strings: List<String> = listOf("first", "second", "fourteenth")
>>> strings.last()
fourteenth
>>> val numbers: Collection<Int> = setOf(1, 14, 2)
>>> numbers.max()
14
fun <T> List<T>.last(): T { /* returns the last element */ }
fun Collection<Int>.max(): Int { /* finding a maximum in a collection */ }

  • varargs: 讓 function 可接受不定數量的參數
fun listOf<T>(vararg values: T): List<T> { ... }

直接在參數前面加上 * ,稱為 spread operator

fun main(args: Array<String>) {
    val list = listOf("args: ", *args)
    println(list)
}

  • infix call
// regular way 呼叫函數
1.to("one")

// infix call
1 to "one"

以下是簡化版 infix 的定義方式

infix fun Any.to(other: Any) = Pair(this, other)

Pair 是 kotlin standard library 的 class,代表一組 elements,例如 val (number, name) = 1 to "one" 可將 1 to "one" 轉換為 Pair 再提供給 (number, name) 設定變數

for ((index, element) in collection.withIndex()) {
    println("$index: $element")
}
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

使用 strings 及 regular expressions

  • splitting strings

kotlin 使用跟 java 一樣的 regular expression syntax

fun main(args: Array<String>) {
    println( "12.345-6.A".split("\\.|-".toRegex()) )
    println( "12.345-6.A".split(".", "-") )
    
    // [12, 345, 6, A]
}
  • 將 file full path name 切割為 directory, filename, extension

使用標準函式庫的 substring functions

fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")

    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")

    println("Dir: $directory, name: $fileName, ext: $extension")
}

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}

使用 regular expression 的方法:regular expression 以 triple-quoted string 撰寫,這樣就不需要 escape characters,所以直接寫 \. 就可以了

fun parsePath(path: String) {
    val regex = """(.+)/(.+)\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path)
    if (matchResult != null) {
        val (directory, filename, extension) = matchResult.destructured
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}

  • multiline triple-quoted strings

在 triple-quoted string 裡面包含所有的字元,trimMargin 可去掉 . 前面所有字元

val kotlinLogo = """| //
                   .|//
                   .|/ \"""

fun main(args: Array<String>) {
    println(kotlinLogo.trimMargin("."))
}

結果為

| //
|//
|/ \

用 embedded expression 填寫 $ 字元

>>> val price = """${'$'}99.9"""
>>> println(price)
$99.9

利用 local functions 及 extensions 簡化程式碼

以下程式碼中,name 及 address 欄位檢查部分的程式碼有重複

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Name")
    }

    if (user.address.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Address")
    }

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

建立一個新的 local function: validate,利用該 local function 檢查欄位

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {

    fun validate(user: User,
                 value: String,
                 fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: empty $fieldName")
        }
    }

    validate(user, user.name, "Name")
    validate(user, user.address, "Address")

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

因為是 local function,可直接使用 user 這個物件

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: " +
                    "empty $fieldName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

將 validate 改成 extension function,讓程式碼更簡潔

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
               "Can't save user $id: empty $fieldName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()

    // Save user to the database
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

References

Kotlin in Action

2019年3月4日

Kotlin Basics

kotlin 基本的 functions, variables, classes, enums, properties,以及型別轉換,及 exception 處理。

functions and variables

這是最基本的 HelloWorld.kt

fun main(args: Array<String>) {
    println("Hello, world!")
}

fun 是宣告 function,參數的型別寫在參數名稱後面,fun不需要放在 class 裡面,可直接放在檔案中,以 println 取代 System.out.println,可省略每一行程式最後面的分號。

kotlin 沒有 array 這種資料型別,必須要使用 Array 這個 class。


  • if 是 expression 不是 statement
fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

fun main(args: Array<String>) {
    println(max(1, 2))
}

以 return 指定 fun 的回傳值

kotlin 的 if 是 expression 不是 statement,差別是 expression 一定有一個 value,可用在另一個 expression 裡面,而 statement 是程式區塊的頂層,沒有 value。所以程式中的 if 可以放在 return 後面,這種寫法在 Java 是不能用的。

kotlin 中,除了 for, do, do/while 以外,都是 expressions,expression 可用在另一個 expression 的功能,可組成 control structures。

另外 assignments 在 Java 是 expressions,但在 kotlin 是 statement。


變數可以不指定資料型別,要刻意指定也可以

val answer = 42
val answer: Int = 42

如果宣告時,沒有指定 value,則宣告變數時就一定要指定資料型別。

val answer: Int
answer = 42

val 是 immutable reference 的變數 var 是 mutable reference 的變數

在 kotlin 應該盡量使用 val,必要時才用 var

要注意的是,immutable 是指 object reference 不變,但是參考到的變數,還是可以改變 value

val languages = arrayListOf("Java")
languages.add("Kotlin")

var 雖然可以改變 object reference,但是仍然要改變為相同資料型別的變數 reference,以下的程式,會出現 type mismatch 的 error

var answer = 42
answer = "no answer"

簡化 string formatting: string templates

fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Kotlin"
    println("Hello, $name!")
}

可以在 string 裡面,用 $name 參考到上面宣告過的變數,但如果是 character,前面要加上 escape,例如 println("\$x")

可用 \({} 加入一個 expression,例如: ```\){args[0]}```

fun main(args: Array<String>) {
    if (args.size > 0) {
        println("Hello, ${args[0]}!")
    }
}

因為 if 是 expression,所以可直接放到 ${} 裡面

fun main(args: Array<String>) {
    println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}

Classes and Properties

class Person(
    val name: String,
    var isMarried: Boolean
)

fun main(args: Array<String>) {
    val person = Person("Bob", true)
    println(person.name)
    println(person.isMarried)
}

以 class 宣告類別,預設就是 public,這個可省略。在 class 後面直接加上 val 參數,表示 name 是 read only 的 field,而宣告為 var 的 isMarried 則是可以改變的,自動就產生了 getter 及 setter。

如果由 Java 使用 Person 這個 class,可直接使用 getter/setter method。

Person person = new Person("Bob", true);
System.out.println(person.getName());  // Bob
System.out.println(person.isMarried()); // true

但是在 kotlin 的 getter/setter,就沒有 get 或 set 的前置,直接用 person.name person.isMarried 就可以了。


可在 class 中,自訂新的 property,並以 get() 撰寫自訂的 getter method,在 Java 是呼叫 isSquare

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() {
            return height == width
        }
}

fun main(args: Array<String>) {
    val rectangle = Rectangle(41, 43)
    println(rectangle.isSquare)
}

Directories and packages

每一個 kotlin .kt 原始檔在開頭可有一個 package 宣告,不同 package 的 kotlin class 必須要 import 後才能使用。

package geometry.shapes

import java.util.Random

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() = height == width
}

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}

可以直接 import top-level function

package geometry.example

import geometry.shapes.createRandomRectangle

fun main(args: Array<String>) {
    println(createRandomRectangle().isSquare)
}

可以將多個 classes 放到同一個 kotlin file 中,kotlin source file name 可以任意自訂,不需要跟 class 名稱一樣。但最好還是遵循 Java 的目錄原則,將同一個 package 的檔案,放到跟 package 相同的目錄中,這樣才能順利找到正確的原始檔。

處理多重選擇: enum 及 when

以 enum class 定義 enum(在 Java 是用 enum)

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

enum 並不是單純的 list of values,可以在 enum 中定義 properties 及 methods。宣告了 enum properties 後,每一個 enum value 都必須指定 properties 的數值,在 enum value 定義後面,一定要加上一個分號,在分號後面才能加上 fun methods

enum class Color(
        // 宣告 enum 的 properties
        val r: Int, val g: Int, val b: Int
) {
    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
    INDIGO(75, 0, 130), VIOLET(238, 130, 238);

    fun rgb() = (r * 256 + g) * 256 + b
}

fun main(args: Array<String>) {
    println(Color.BLUE.rgb())
}

when 是用來取代 Java 的 switch 語法

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

fun getMnemonic(color: Color) =
    when (color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "Of"
        Color.YELLOW -> "York"
        Color.GREEN -> "Gave"
        Color.BLUE -> "Battle"
        Color.INDIGO -> "In"
        Color.VIOLET -> "Vain"
    }

fun main(args: Array<String>) {
    println(getMnemonic(Color.BLUE))
}

可將多個 enum 對應到同一個 value

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

fun getWarmth(color: Color) = when(color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.GREEN -> "neutral"
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

fun main(args: Array<String>) {
    println(getWarmth(Color.ORANGE))
}

在 import 時,必須要 import Color 這個 class,另外再 import enum constants

import ch02.colors.Color
import ch02.colors.Color.*

fun getWarmth(color: Color) = when(color) {
    RED, ORANGE, YELLOW -> "warm"
    GREEN -> "neutral"
    BLUE, INDIGO, VIOLET -> "cold"
}

fun main(args: Array<String>) {
    println(getWarmth(Color.ORANGE))
}

可將 when 用在任意的 object(Java 的 switch 只能用在 enum constants, strings, number literals)。

when 裡面是 setOf 集合,後面每一個條件,都必須同樣檢查為 setOf,最後用 else 設定其他的條件。

import ch02.colors.Color
import ch02.colors.Color.*

fun mix(c1: Color, c2: Color) =
        when (setOf(c1, c2)) {
            setOf(RED, YELLOW) -> ORANGE
            setOf(YELLOW, BLUE) -> GREEN
            setOf(BLUE, VIOLET) -> INDIGO
            else -> throw Exception("Dirty color")
        }

fun main(args: Array<String>) {
    println(mix(BLUE, YELLOW))
}

上面的 code 會產生一些 Set 物件,為了程式效能,可去掉 when 的參數,直接

import ch02.colors.Color
import ch02.colors.Color.*

fun mixOptimized(c1: Color, c2: Color) =
    when {
        (c1 == RED && c2 == YELLOW) ||
        (c1 == YELLOW && c2 == RED) ->
            ORANGE

        (c1 == YELLOW && c2 == BLUE) ||
        (c1 == BLUE && c2 == YELLOW) ->
            GREEN

        (c1 == BLUE && c2 == VIOLET) ||
        (c1 == VIOLET && c2 == BLUE) ->
            INDIGO

        else -> throw Exception("Dirty color")
    }

fun main(args: Array<String>) {
    println(mixOptimized(BLUE, YELLOW))
}

smart casts: 合併了 type checks 及 casts

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int {
    if (e is Num) {
        // 將 e 轉型為 Num,可以省略
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

fun main(args: Array<String>) {
    println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}

一開始定義了兩個 class 分別都實作 Expr 介面, eval 函數區分了 Num 與 Sum 的回傳值。

is 是在檢查資料型別的保留字,且在通過檢查後,compiler 會自動將 e 轉型為該資料型別。 as 是強制轉型的保留字。

eval(Sum(Sum(Num(1), Num(2)), Num(4))) 就是在計算 (Num(1) + Num(2)) + Num(4) 的結果。

將 if 取代為 when

在 kotlin 的 if 是 expression,會有 return value。所以可以將剛剛的 eval 改寫為 if expression。

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
    if (e is Num) {
        e.value
    } else if (e is Sum) {
        eval(e.right) + eval(e.left)
    } else {
        throw IllegalArgumentException("Unknown expression")
    }

fun main(args: Array<String>) {
    println(eval(Sum(Num(1), Num(2))))
}

剛剛的 if 裡面,都只有一個 expression,可以用 when 改寫。

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
    when (e) {
        is Num ->
            e.value
        is Sum ->
            eval(e.right) + eval(e.left)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

fun main(args: Array<String>) {
    println(eval(Sum(Num(1), Num(2))))
}

when 的 is 裡面,超過一個 expression,就用 { } 包括在一起,最後一個 statement 就是 return value。

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun evalWithLogging(e: Expr): Int =
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value
        }
        is Sum -> {
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: $left + $right")
            left + right
        }
        else -> throw IllegalArgumentException("Unknown expression")
    }

fun main(args: Array<String>) {
    println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
}

while 及 for loops

1..100 是 ranges,由 1 到 100

100 downTo 1 step 2,是由 100 到 1,每一次都 -2

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz "
    i % 3 == 0 -> "Fizz "
    i % 5 == 0 -> "Buzz "
    else -> "$i "
}

fun main(args: Array<String>) {
    for (i in 1..100) {
        print(fizzBuzz(i))
    }
    
    for (i in 100 downTo 1 step 2) {
        print(fizzBuzz(i))
    }
}

for (x in 0 until size) 也可以寫成 for (x in 0..size-1)


如何 iterate maps

import java.util.TreeMap

fun main(args: Array<String>) {
    val binaryReps = TreeMap<Char, String>()

    for (c in 'A'..'F') {
        val binary = Integer.toBinaryString(c.toInt())
        binaryReps[c] = binary
    }

    for ((letter, binary) in binaryReps) {
        println("$letter = $binary")
    }
    
    val list = arrayListOf("10", "11", "1001")
    for ((index, element) in list.withIndex()) {
        println("$index: $element")
    }
}

執行結果

A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110
0: 10
1: 11
2: 1001

用 in !in 檢查 ranges,也可以用在 when 裡面

"Kotlin" in "Java".."Scala" Kotlin 是在 Java 到 Scala 範圍之間,所有字串的一員

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's a digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know…​"
}

fun main(args: Array<String>) {
    println("q isLetter: "+isLetter('q'))
    println("x isNotDigit: "+isNotDigit('x'))

    println()
    println("8:"+ recognize('8'))

    println()
    println("Kotlin" in "Java".."Scala")

    println()
    println("Kotlin" in setOf("Java", "Scala"))
}

執行結果

q isLetter: true
x isNotDigit: true

8:It's a digit!

true

false

Exceptions

kotlin 的 throw,後面不需要加上 new 再搭某一個 Exception class

if (percentage !in 0..100) {
    throw IllegalArgumentException(
        "A percentage value must be between 0 and 100: $percentage")
}

跟 Java 類似,也適用 try ... catch ... finally 的語法,但是在 method 宣告的地方,不需要宣告該 method function 可能會 throw Exception。

readNumber 這個 method 是回傳 Int? ,增加 ? 的用意,是表示該 method 會回傳整數或是 null。

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)
    }
    catch (e: NumberFormatException) {
        return null
    }
    finally {
        reader.close()
    }
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("239"))
    println(readNumber(reader))
}

要改變習慣,將 try 當作 expression,以往會在 catch Exception 的部分,直接 return

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        return
    }

    println(number)
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("not a number"))
    readNumber(reader)
}

將 try 的 catch exception 部分,直接寫成 null,就表示 number 是 Int 或是 null。

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null
    }

    println(number)
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("not a number"))
    readNumber(reader)
}

References

Kotlin in Action

2019年2月25日

Introduction to Kotlin

2011年7月,JetBrains推出 Kotlin 專案,Kotlin 是一種在 Java VM 上執行的靜態型別程式語言,它也可以被編譯成為 JavaScript 原始碼。它是由俄羅斯聖彼得堡的 JetBrains 開發團隊所發展出來的程式語言,其名稱來自於聖彼得堡附近的科特林島。雖然與Java語法並不相容,但Kotlin被設計成可以和Java程式碼相互運作,並可以重複使用如Java集合框架等的現有Java類別庫。

相容於 Java VM 的語言還有很多,語言特性只有 Scala 比較符合需求,然而 Scala 本身有編譯太慢的缺陷,因此 Kotlin 的設計目標就是希望能跟 Java 一樣,快速編譯。JetBrains 開發這語言的用意,是希望這個新語言能夠推動 IntelliJ IDEA 的銷售。自 2011年開始開發 Kotlin 以來,一直到了 2016年2月15日,Kotlin 才釋出第一個穩定的 1.0 版,而且官方認為在未來的更新版本,都會往前相容。

Oracle 針對 Android 侵權,對 Google 提出訴訟

Oracle 在 2010 年收購 Sun Microsystems 之後不久,在就對 Google 提起訴訟,認為他們在 Android 平台上使用 37 項 Java API 侵犯了他們的權益,求償 26 億美元,Sun Microsystems 公司是構建 Java 語言和平台的公司。到了 2016 年這個數字又再以大量成長的 Android 裝置數量來調整,求償金額高達 93 億美元,2016年五月,舊金山聯邦法庭陪審團認定安卓合理利用JavaAPI並未侵權,Oracle 再次敗訴。2016年10月,Oracle 再次向上訴至聯邦巡迴上訴法院。2018年3月,聯邦巡迴上訴法院再次裁定支持 Oracle,認定 Google 侵權。

為了解決這樣的問題,Google 在 2017 年 Google I/O 宣布正式把 Kotlin 納入 Android 程式的官方一級開發語言(First-class language),因為是全新的語言,語法不相容於 Java,有新的 API,可以編譯為 java class file,與既有的 Java Library 交互運作,這樣就可以迴避 Oracle 宣稱的 API 侵權問題。

Kotline 建構流程

Kotlin 原始程式碼是以 .kt 為副檔名儲存的,透過 compiler 編譯為 class,這個步驟跟 Java 一樣,到最後要執行時,再引用 Kotlin Runtime 就可以運作。

開發工具

Kotline 可搭配 Intellij IDEA、Android Studio 或是 Eclipse 這幾種 IDE 進行開發,也可以自己安裝 Command Line Compiler

Command Line Compiler 可直接在 github 下載,解壓縮後就可以用,如果在 Mac OS,可以用 Homebrew 或是 MacPorts 安裝。

sudo port install kotlin

簡單寫個 hello world 測試

vi hello.kt

fun main(args: Array<String>) {
    println("Hello, World!")
}

編譯時加上 -include-runtime 會將 Kotlin Runtime Library 直接包裝在 hello.jar 裡面,可直接用 java 執行。

$ kotlinc hello.kt -include-runtime -d hello.jar
$ java -jar hello.jar
Hello, World!

如果直接編譯,沒有引入 Runtime Library,則需要在執行時,透過 kotlin 執行。

$ kotlinc hello.kt -d hello.jar
$ kotlin -classpath hello.jar HelloKt
Hello, World!

另外有一個 kotlinc-jvm REPL 的 command line 工具

$ kotlinc-jvm
Welcome to Kotlin version 1.2.41 (JRE 1.8.0_152-b16)
Type :help for help, :quit for quit
>>> 2+2
4
>>> println("Hello")
Hello

Kotlin 也支援 scripting 形式的 code,以 .kts 為副檔名

vi files.kts

import java.io.File

val folders = File(args[0]).listFiles { file -> file.isDirectory() }
folders?.forEach { folder -> println(folder) }

透過 kotlinc 執行

$ kotlinc -script files.kts .

Kotlin Koans

Kotlin Koans 是個簡單的教學 tutorial,可用 IDE 以 Gradle project 方式 import project,然後就能透過 Unit Test 的方式,一步一步接受指令完成任務。

舉例來說,在 src/iintroduction/0HelloWorld/n00Start.kt 可看到第一個任務,只要把 task0 改成回傳 "OK",然後到 test/iintroduction/0HelloWorld/n00StartKtTest.kt ,進行單元測試,如果測試通過,就是完成這個任務。

fun task0(): String {
    return "OK"
}

如果不想使用 IDE,也可以在 try.kotlinlang.org 直接線上進行 Kotlin Koans。

Java-Kotlin 自動轉換

在 IDE 有個功能,可以自動將 Java 的 code 轉換為 Kotlin,這是因為 Kotlin 本身的語法就是從 Java 簡化而來的,所以能夠將 Java 的 code 直接一對一轉換為 Kotlin,但有個問題是,這樣的轉換,沒辦法轉換為 Kotlin 的最簡語法。

Kotlin Koans 第一號任務,就是在教我們如何使用這個自動轉換的功能。只要直接複製 Java code,在 IDE 中,貼到 kotlin source 中,IDE 就會自動將 Java code 轉換為 Kotlin。

Kotlin 發展哲學

  • Pragmatic 實用

    Kotlin 借用了 Scala 的一些語法概念,發展自己的語法,但跟 Scala 不同的是 Scala 是一種學院派的語言,他有著許多神奇的語法結構,導致要入門 Scala 需要一段很長的時間。

    Kotlin 不同,完全是為了搭配 IDE 而實作的,因此語法也僅止於簡化 Java,改良 Java 遇到的一些問題。kotlin 是以實用為主的語言。

  • Concise 簡潔

    簡單清楚的程式碼,減輕了開發者的負擔,增加了程式碼可讀性,也縮短了開發與除錯的時間,工作效率更高。

  • Safe 安全

    Kotlin 是一個靜態資料型別的語言,確保了程式的資料型別安全,但不需要在程式碼中指定資料型別,因為 compiler 可以自己判斷。

    Kotlin 改良了傳統 Java 的問題,移除了 NullPointerException,以及 ClassCastException,避免程式在運作期間,才出現這些 Exception 導致程式異常中斷。

  • Interoperable 互操作性

    Kotlin 能夠使用既有 Java 領域的所有 Library,同時提供了 Java code 轉換為 Kotlin 的工具,因為 Kotlin 會編譯為 Java 的 class file,運作在 JVM 上,這表示在運作過程中,跟以往的 Java Applcation 完全是一樣的。

References

Kotlin實戰(Kotlin in action)

Kotlin Koans學習筆記

Kotlin Wiki

Java 世紀侵權案甲骨文勝訴,向全球追討授權費台灣也遭殃

2019年2月18日

Keras 手寫阿拉伯數字辨識

keras 是 python 語言的機器學習套件,後端能使用 Google TensorFlow, Microsoft CNTKTheano 運作。其中 Theano 在 2017/9/28 就宣佈在 1.0 後就不再更新。一般在初學機器學習時,都是用手寫阿拉伯數字 MNIST 資料集進行測試,kaggle Digit Recognizer 有針對 MNIST data 的機器學習模型的評比,比較厲害的,都可以達到 100% 的預測結果。

CentOS 7 Keras, TensorFlow docker 測試環境

docker run -it --name c1 centos:latest /bin/bash

安裝一些基本工具,以及 openssh-server

#yum provides ifconfig

yum install -y net-tools telnet iptables sudo initscripts
yum install -y passwd openssl openssh-server

yum install -y wget vim

測試 sshd

/usr/sbin/sshd -D
Could not load host key: /etc/ssh/ssh_host_rsa_key
Could not load host key: /etc/ssh/ssh_host_ecdsa_key
Could not load host key: /etc/ssh/ssh_host_ed25519_key

缺少了一些 key

ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
#直接 enter 即可

ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
#直接 enter 即可

ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N ""

ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

修改 UsePAM 設定

vi /etc/ssh/sshd_config
# UsePAM yes 改成 UsePAM no
UsePAM no

再測試看看 sshd

/usr/sbin/sshd -D&

修改 root 密碼

passwd root

離開 docker

exit

以 docker ps -l 找到剛剛那個 container 的 id

$ docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
107fb9c3fc0d        centos:latest       "/bin/bash"         7 minutes ago       Exited (0) 2 seconds ago                       c1

將 container 存成另一個新的 image

docker commit 107fb9c3fc0d centosssh

以新的 image 啟動另一個 docker instance

(port 10022 是 ssh,15900 是 vnc)

(--privileged=true 是避免 systemd 發生的 Failed to get D-Bus connection: Operation not permitted 問題)

docker run -d -p 10022:22 -p 15900:5900 -e "container=docker" --ulimit memlock=-1 --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name test centosssh /usr/sbin/init

docker exec -it test /bin/bash

現在可以直接 ssh 登入新的 docker machine

ssh root@localhost -p 10022

修改 timezone, locale

timedatectl set-timezone Asia/Taipei

把 yum.conf 的 overrideinstalllangs 註解掉

vi /etc/yum.conf

#override_install_langs=en_US.utf8
yum -y -q reinstall glibc-common
localectl list-locales|grep zh
# 會列出所有可設定的 locale
zh_CN
zh_CN.gb18030
zh_CN.gb2312
zh_CN.gbk
zh_CN.utf8
zh_HK
zh_HK.big5hkscs
zh_HK.utf8
zh_SG
zh_SG.gb2312
zh_SG.gbk
zh_SG.utf8
zh_TW
zh_TW.big5
zh_TW.euctw
zh_TW.utf8

# 將 locale 設定為 zh_TW.utf8
localectl set-locale LANG=zh_TW.utf8

安裝視窗環境及VNC

ref: https://www.jianshu.com/p/38a60776b28a

yum groupinstall -y "GNOME Desktop"

# 預設啟動圖形介面
unlink /etc/systemd/system/default.target
ln -sf /lib/systemd/system/graphical.target /etc/systemd/system/default.target

# 安裝 vnc server
yum -y install tigervnc-server tigervnc-server-module 

# vnc 預設的port tcp 5900,則組態檔複製時在檔名中加入0,如vncserver@:0.service,如果要使用其他的port,就把0改為其他號碼
cp /lib/systemd/system/vncserver@.service /etc/systemd/system/vncserver@:0.service

vi /etc/systemd/system/vncserver@:0.service
# 修改中間的部分
ExecStartPre=/bin/sh -c '/usr/bin/vncserver -kill %i > /dev/null 2>&1 || :'
ExecStart=/usr/sbin/runuser -l root -c "/usr/bin/vncserver %i -geometry 1280x1024"
PIDFile=/root/.vnc/%H%i.pid
ExecStop=/bin/sh -c '/usr/bin/vncserver -kill %i > /dev/null 2>&1 || :'


# 執行 vncpasswd 填寫 vnc 密碼
su root
vncpasswd

# 退出 container
exit

# restart docker container
docker restart test

# 進入 docker container
docker exec -it test /bin/bash

# 啟動 service
systemctl daemon-reload
systemctl start vncserver@:0.service
systemctl enable vncserver@:0.service

# 開啟防火牆允許VNC的連線,以及重新load防火牆,這邊多開放了port 5909。
firewall-cmd --permanent --add-service="vnc-server" --zone="public"
#firewall-cmd --add-port=5909/tcp --permanent
firewall-cmd --reload

vncserver -list

# 以 vnc client 連線,連接 localhost:15900

如果用 vnc 連線到 docker 機器,後面測試時,matplotlib 可直接把圖形畫在視窗上,就不用存檔。

安裝 TensorFlow, python 3.6 開發環境

yum -y install centos-release-scl
yum -y install rh-python36

python --version
# Python 2.7.5

# 目前還是 python 2.7,必須 enable 3.6
scl enable rh-python36 bash

python --version
# Python 3.6.3

但每次登入都還是 2.7

vi /etc/profile.d/rh-python36.sh

#!/bin/bash
source scl_source enable rh-python36

接下來每次登入都是 3.6

安裝 TensorFlow

pip3 install --upgrade tensorflow

# 更新 pip
pip3 install --upgrade pip

簡單測試,是否有安裝成功

# python
import tensorflow as tf
hello = tf.constant('Hello, TensorFlow!')
sess = tf.Session()
print(sess.run(hello))

# 會列印出這樣的結果
# b'Hello, TensorFlow!'

#---------

python -c "import tensorflow as tf; tf.enable_eager_execution(); print(tf.reduce_sum(tf.random_normal([1000, 1000])))"
# 會列印出這樣的結果
# tf.Tensor(12.61731, shape=(), dtype=float32)

再安裝 keras, matplotlib (需要 tk library)

pip3 install keras

yum -y install rh-python36-python-tkinter
pip3 install matplotlib

阿拉伯數字辨識

MNIST 是一個包含 60,000 training images 及 10,000 testing images 的手寫阿拉伯數字的測試資料集。資料集的每個圖片都是解析度為 28*28 (784 個 pixel) 的灰階影像, 每個像素為 0~255 之數值。

One-Hot Encoding 就是一位有效編碼,當有 N 種狀態,就使用 N 位狀態儲存器的編碼,每一個狀態都有固定的位置。

例如阿拉伯數字就是 0 ~ 9,就使用這樣的編碼方式

[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.]    代表 0
...
[0., 0., 0., 0., 0., 1., 0., 0., 0., 0.]    代表 5
...
[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]    代表 9

程式處理的步驟如下:

  1. 取得訓練資料:目前是直接使用既有的 MNIST 資料集,利用這些既有的資料,進行機器學習。
  2. 機器訓練,取得模型:進行機器訓練,取得訓練後結果的模型,未來就可以利用這個模型,判斷新進未知的資料
  3. 評估:利用 MNIST 資料集的測試資料,評估模型判斷後的結果跟正確結果的差異,取得這個模型的準確率。
  4. 預測:未來可利用這個模型,判斷並預測新進資料的結果。當然這會因為上一個步驟的準確度,有時候會失準,不一定會完全正確。

以下這個例子是使用 Sequential 線性的模型,input layer 是 MNIST 60000 筆訓練資料,中間是一層有 256 個變數的 hidden layer,最後是 10 個變數 (0~9) 的 output layer。機器學習就是在產生 input layer 到 hidden layer,以及 hidden layer 到 output layer 中間的 weight 權重。

input layer --- W(i,j) ---> hidden layer (256 個變數) --- W(j, k) ---> output layer (0~9) 

測試程式 test.py

import numpy as np
from keras.models import Sequential
from keras.datasets import mnist
from keras.layers import Dense, Dropout, Activation, Flatten
# 用來後續將 label 標籤轉為 one-hot-encoding
from keras.utils import np_utils
from matplotlib import pyplot as plt

# 載入 MNIST 資料庫的訓練資料,並分為 training 60000 筆 及 testing 10000 筆 data
(x_train, y_train), (x_test, y_test) = mnist.load_data()


# 將 training 的 label 進行 one-hot encoding,例如數字 7 經過 One-hot encoding 轉換後是 array([0., 0., 0., 0., 0., 0., 0., 1., 0., 0.], dtype=float32),即第7個值為 1
y_train_onehot = np_utils.to_categorical(y_train)
y_test_onehot = np_utils.to_categorical(y_test)

# 將 training 的 input 資料轉為 28*28 的 2維陣列
# training 與 testing 資料數量分別是 60000 與 10000 筆
# X_train_2D 是 [60000, 28*28] 的 2維陣列
x_train_2D = x_train.reshape(60000, 28*28).astype('float32')
x_test_2D = x_test.reshape(10000, 28*28).astype('float32')

x_train_norm = x_train_2D/255
x_test_norm = x_test_2D/255


# 建立簡單的線性執行的模型
model = Sequential()
# Add Input layer, 隱藏層(hidden layer) 有 256個輸出變數
model.add(Dense(units=256, input_dim=784, kernel_initializer='normal', activation='relu'))
# Add output layer
model.add(Dense(units=10, kernel_initializer='normal', activation='softmax'))

# 編譯: 選擇損失函數、優化方法及成效衡量方式
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])


# 進行 model 訓練, 訓練過程會存在 train_history 變數中
# 將 60000 張 training set 的圖片,用 80% (48000張) 訓練模型,用 20% (12000張) 驗證結果
# epochs 10 次,就是訓練做了 10 次
# batch_size 是 number of samples per gradient update,每一次進行 gradient descent 使用幾個 samples
# verbose 是 train_history 的 log 顯示模式,2 表示每一輪訓練,列印一行 log
train_history = model.fit(x=x_train_norm, y=y_train_onehot, validation_split=0.2, epochs=10, batch_size=800, verbose=2)

# 用 10000 筆測試資料,評估訓練後 model 的成果(分數)
scores = model.evaluate(x_test_norm, y_test_onehot)
print()
print("Accuracy of testing data = {:2.1f}%".format(scores[1]*100.0))

# 預測(prediction)
X = x_test_norm[0:10,:]
predictions = model.predict_classes(X)
# get prediction result
print()
print(predictions)

# 模型訓練結果 結構存檔
from keras.models import model_from_json
json_string = model.to_json()
with open("model.config", "w") as text_file:
    text_file.write(json_string)

# 模型訓練結果 權重存檔
model.save_weights("model.weight")


# 顯示 第一筆訓練資料的圖形,確認是否正確
#plt.imshow(x_train[0])
#plt.show()
#plt.imsave('1.png', x_train[0])

plt.clf()

plt.plot(train_history.history['loss'])
plt.plot(train_history.history['val_loss'])
plt.title('Train History')
plt.ylabel('loss')
plt.xlabel('Epoch')
plt.legend(['loss', 'val_loss'], loc='upper left')
#plt.show()
plt.savefig('loss.png')

執行結果

# python test.py
Using TensorFlow backend.
Train on 48000 samples, validate on 12000 samples
Epoch 1/10
 - 2s - loss: 0.7582 - acc: 0.8134 - val_loss: 0.3195 - val_acc: 0.9117
Epoch 2/10
 - 1s - loss: 0.2974 - acc: 0.9160 - val_loss: 0.2473 - val_acc: 0.9307
Epoch 3/10
 - 2s - loss: 0.2346 - acc: 0.9350 - val_loss: 0.2060 - val_acc: 0.9425
Epoch 4/10
 - 2s - loss: 0.1930 - acc: 0.9465 - val_loss: 0.1741 - val_acc: 0.9522
Epoch 5/10
 - 2s - loss: 0.1631 - acc: 0.9539 - val_loss: 0.1529 - val_acc: 0.9581
Epoch 6/10
 - 2s - loss: 0.1410 - acc: 0.9604 - val_loss: 0.1397 - val_acc: 0.9612
Epoch 7/10
 - 1s - loss: 0.1225 - acc: 0.9662 - val_loss: 0.1301 - val_acc: 0.9639
Epoch 8/10
 - 1s - loss: 0.1075 - acc: 0.9695 - val_loss: 0.1171 - val_acc: 0.9668
Epoch 9/10
 - 1s - loss: 0.0948 - acc: 0.9744 - val_loss: 0.1123 - val_acc: 0.9681
Epoch 10/10
 - 1s - loss: 0.0855 - acc: 0.9771 - val_loss: 0.1047 - val_acc: 0.9700
10000/10000 [==============================] - 1s 57us/step

Accuracy of testing data = 97.1%

[7 2 1 0 4 1 4 9 6 9]

Model Persistence

要儲存訓練好的模型,有兩種方式

  1. 結構及權重分開儲存

    儲存模型結構,可儲存為 JSON 或 YAML

    from keras.models import model_from_json
    json_string = model.to_json()
    with open("model.config", "w") as text_file:
      text_file.write(json_string)

    儲存權重

    model.save_weights("model.weight")

    讀取結構及權重

    import numpy as np  
    from keras.models import Sequential
    from keras.models import model_from_json
    with open("model.config", "r") as text_file:
      json_string = text_file.read()
      model = Sequential()
      model = model_from_json(json_string)
      model.load_weights("model.weight", by_name=False)
  2. 合併儲存結構及權重

    合併儲存時,檔案格式為 HDF5

    from keras.models import load_model
    
    model.save('model.h5')  # creates a HDF5 file 'model.h5'

    讀取模型

    from keras.models import load_model
    
    # 載入模型
    model = load_model('model.h5')

References

【深度學習框架 Theano 慘遭淘汰】微軟數據分析師:為何曾經熱門的 Theano 18 個月就陣亡?

【Python】CentOS7 安裝 Python3

Install TensorFlow with pip

撰寫第一支 Neural Network 程式 -- 阿拉伯數字辨識

MyNeuralNetwork/0.py

改善 CNN 辨識率

mnist-cnn/mnist-CNN-datagen.ipynb

深度學習 TensorFlow

2019年2月11日

erlang 如何支援多個設定檔

通常 erlang project 會將設定的資料放在 sys.config 裡面,同樣的,很多 library 也會在說明文件裡面提到,要將該 library 相關的設定資訊,寫在 sys.config 裡面,在規模稍大的 project 就會看到一個很冗長複雜的設定檔。

但其實在 elang config 的標準文件 sys.config 說明裡面有提到,sys.config 裡面的語法是

[{Application, [{Par, Val}]} | File].

File 的部分就是附加的設定檔,erlang 會將所有設定檔裡面,對於某個 application 相關的所有參數,依照檔案順序整合在一起

首先利用 rebar 產生一個測試的 application myapp,並進行編譯

$ rebar create-app appid=myapp
==> erltest (create-app)
Writing src/myapp.app.src
Writing src/myapp_app.erl
Writing src/myapp_sup.erl

$ rebar compile
==> erltest (compile)
Compiled src/myapp_app.erl
Compiled src/myapp_sup.erl

然後依照以往的做法,將 myapp 的參數寫在 sys.config 裡面

[
    { myapp, [
        {par1,val1},
        {par2,val2}
    ]},
    { myapp, [
        {par1,newval1},
        {par3,newval3}
    ]}
].

注意,我們刻意將 myapp 的參數分成兩個部分撰寫,然後我們可以在 console 啟動 myapp,並利用 application:get_all_env(myapp). 取得 myapp 所有的參數進行測試。

$ erl -pa ebin -config sys
1> application:get_all_env(myapp).
[]
2> application:load(myapp).
ok
3> application:get_all_env(myapp).
[{par2,val2},
 {par1,newval1},
 {included_applications,[]},
 {par3,newval3}]

透過測試結果可以發現 myapp 的三個參數,是依照參數定義的順序累加並覆蓋得到的。


接下來,我們建立一個新的設定檔 conf/node1.config,內容為

[
    { myapp, [
        {par1,node1_val1},
        {par2,node1_val2},
        {parN,node1_valN}
    ]}
].

project 的目錄及檔案是這樣,也就是增加了一個 conf 的目錄

conf\
    node1.config
ebin\
src\
    myapp_app_erl
    myapp_sup.erl
    myapp_app.src
sys.config

然後修改 sys.config,把 node1.config 的設定附加在後面

[
    { myapp, [
        {par1,val1},
        {par2,val2}
    ]},
    { myapp, [
        {par1,newval1},
        {par3,newval3}
    ]}
    , "conf/node1.config"
].

這時候測試得到的結果會發現,node1.config 的設定覆蓋了原本 sys.config 裡面的設定

$ erl -pa ebin -config sys
1> application:get_all_env(myapp).
[]
2> application:load(myapp).
ok
3> application:get_all_env(myapp).
[{par2,node1_val2},
 {par1,node1_val1},
 {included_applications,[]},
 {par3,val3},
 {parN,node1_valN}]

我們可以將 sys.config 的機制,運用在兩個地方

  1. 分離 library 的設定檔 將某些 library 的設定,以獨立的設定檔方式撰寫
  2. 覆寫獨立的 node 的設定檔 將新的 node 的設定檔,也就是每一個 node 不同的設定資料,不修改既有的 sys.config 而是寫在 node 的設定檔,覆寫既有的設定

不過如果檔案的部分可以支援這樣的寫法 "conf/*.config",對於 library 設定檔的整合,會比較簡單。但如果支援了 *.config,就無法自定設定檔的引用順序,可能只能依照字母的順序排序了。

References

A little known fact about Erlang's sys.config

An easy way to handle configuration parameters in Erlang

節點啟動後自動連接其它配置節點

[erlang-questions] Multiple application configurations in multiple files