2019/04/29

julia1 installation

安裝

直接到官方網站下載 dmg,然後安裝就好了。

如果要移除,根據 macOS Help 的說明,只要將 Julia.app 刪除,並刪除 ~/.julia 目錄裡面的 packages 就可以了,preferences files 為 ~/.juliarc.jl


另外可在 mac 以 macport 安裝

sudo port install julia

目前的版本為 0.6.2,julia @0.6.2_2: update to 1.0.0 已經有更新到正式版 1.0.0 的 task,還沒完成。

REPL

REPL (Read-Eval-Print-Loop) 互動的 shell,有一個 JIT compiler 可以即時編譯 julia 並 evaluate 程式。

$ julia
               _
   _       _ _(_)_     |  A fresh approach to technical computing
  (_)     | (_) (_)    |  Documentation: https://docs.julialang.org
   _ _   _| |_  __ _   |  Type "?help" for help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 0.6.2 (2017-12-13 18:08 UTC)
 _/ |\__'_|_|_|\__'_|  |
|__/                   |  x86_64-apple-darwin17.7.0
julia>

IDE

sublime text

安裝 Julia native API for ZMQ ØMQ

在 julia REPL 裡面安裝 ZMQ package

julia> using Pkg
julia> Pkg.add("ZMQ")

在 Sublime Text 的 Preferences -> Package Control -> Install Package,搜尋 Julia 並安裝 Julia syntax highlighting for Sublime Text 2/3

重新啟動 Sublime Text 會出現 Error Alert

ZMQ Shared Library not found....

到 Preference -> Package Settings -> Sublime-IJulia -> Settings-Default,修改 osx 項目裡面的 zmqsharedlibrary 的位置

"zmq_shared_library": "~/.julia/packages/ZMQ/ABGOx/deps/usr/lib/libzmq.dylib",
IDEA

安裝 Julia Plugin,然後到 Prefrences -> Languages & Frameworks -> Julia 裡面調整 Julia Configuration

因為是使用 macport 安裝的,所以將 Julia executable 設定為 /Applications/Julia-1.0.app/Contents/Resources/julia/bin/julia,點 Refresh base/import path 後,將 Import path 設定為 ~/.julia/environments/v1.0,Julia base path 設定為 /Applications/Julia-1.0.app/Contents/Resources/julia/share/julia/base

測試程式

hello.jl

function helloworld()
    println("Hello world from hello.jl")
end

進入 julia console

julia> include("hello.jl")
helloworld (generic function with 1 method)

julia> helloworld()
Hello world from hello.jl

~/.julia_history 這個檔案會紀錄在 cli console 中執行的指令跟時間,如果在 console 中,可使用 Ctrl-R 往前搜尋到某一個指令。

help and shell

在 console 中輸入 ? ,提示符號會變成 help?>,然後填入要查詢的 function/types/macros/operators

在 console 中輸入 ; ,提示符號會變成 shell>,可輸入 shell command。

其他的指令

# 查詢 method 的參數
julia> @which sin(10)
sin(x::Real) in Base.Math at special/trig.jl:53

# 查詢版本資訊
julia> versioninfo()
Julia Version 1.0.1
Commit 0d713926f8 (2018-09-29 19:05 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin14.5.0)
  CPU: Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, ivybridge)
Environment:
  JULIA_HOME = /Applications/0programming/Julia-1.0.app/Contents/Resources/julia

# 呼叫系統editor
julia> edit("hello.jl")

# 呼叫 less
julia> less("hello.jl")

# 呼叫系統 clipboard()
julia> clipboard("測試")

Main,Core和Base 是 julia 基本的三個 module,啟動 REPL 就在 Main 裡面,Core 是核心,Base 是標準 library。

# 查詢該 module 可使用的 fieldnames propertynames nameof NamedTuple dirname tempname fullname basename
julia> names(Main)
4-element Array{Symbol,1}:
 :Base
 :Core
 :InteractiveUtils
 :Main

julia> dump(Main)
Module Main
# 查詢某個 DataType 的 fieldnames
julia> fieldnames(Rational)
(:num, :den)

Plots in REPL

安裝 UnicodePlots

julia> using Pkg

julia> Pkg.add("UnicodePlots")

Unicode Plot 可支援 scatterplots, line plots, histograms...

julia> using UnicodePlots

julia> lineplot([cos, sin], -pi/2, 2pi)
           ┌────────────────────────────────────────┐
         1 │⠀⠀⠀⠀⠀⠀⢀⠖⢹⠉⢢⠀⠀⢀⠞⠉⠉⢢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠞⠉⠀⠀⠀│ cos(x)
           │⠀⠀⠀⠀⠀⢠⠊⠀⢸⠀⠀⠳⣠⠊⠀⠀⠀⠀⠣⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀│ sin(x)
           │⠀⠀⠀⠀⢀⠇⠀⠀⢸⠀⠀⢠⢷⠀⠀⠀⠀⠀⠀⢱⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠇⠀⠀⠀⠀⠀⠀│
           │⠀⠀⠀⠀⡜⠀⠀⠀⢸⠀⠀⡜⠀⢧⠀⠀⠀⠀⠀⠀⢧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀⠀⠀⠀⠀⠀⠀│
           │⠀⠀⠀⢸⠀⠀⠀⠀⢸⠀⢸⠀⠀⠘⡄⠀⠀⠀⠀⠀⠘⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⡸⠀⠀⠀⠀⠀⠀⠀⠀│
           │⠀⠀⢀⠇⠀⠀⠀⠀⢸⢀⠇⠀⠀⠀⢱⠀⠀⠀⠀⠀⠀⢱⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀│
           │⠀⠀⡜⠀⠀⠀⠀⠀⢸⡜⠀⠀⠀⠀⠈⡆⠀⠀⠀⠀⠀⠈⡆⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀│
   f(x)    │⠤⠤⠧⠤⠤⠤⠤⠤⢼⠧⠤⠤⠤⠤⠤⠼⡤⠤⠤⠤⠤⠤⠼⡤⠤⠤⠤⠤⠤⢴⠥⠤⠤⠤⠤⠤⢤⠤⠤⠄│
           │⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀│
           │⠀⠀⠀⠀⠀⠀⠀⡸⢸⠀⠀⠀⠀⠀⠀⠀⠈⡆⠀⠀⠀⠀⠀⠈⡆⠀⠀⠀⡸⠀⠀⠀⠀⠀⠀⡸⠀⠀⠀⠀│
           │⠀⠀⠀⠀⠀⠀⢀⠇⢸⠀⠀⠀⠀⠀⠀⠀⠀⢱⠀⠀⠀⠀⠀⠀⢱⠀⠀⢠⠃⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀│
           │⠀⠀⠀⠀⠀⠀⡞⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⢇⠀⠀⠀⠀⠀⠀⢇⠀⡞⠀⠀⠀⠀⠀⠀⡎⠀⠀⠀⠀⠀│
           │⠀⠀⠀⠀⠀⡸⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡆⠀⠀⠀⠀⠀⠘⡾⠀⠀⠀⠀⠀⠀⡸⠀⠀⠀⠀⠀⠀│
           │⠀⠀⠀⠀⡰⠁⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢄⠀⠀⠀⠀⡜⠙⣄⠀⠀⠀⠀⡜⠁⠀⠀⠀⠀⠀⠀│
        -1 │⠀⢀⣀⠜⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢦⣀⣀⠜⠀⠀⠈⢦⣀⣠⠜⠀⠀⠀⠀⠀⠀⠀⠀│
           └────────────────────────────────────────┘
           -2                                       7
                               x

pi 就是圓周率 π ,julia 可支援使用這個 unicode 符號。他是借用 LaTeX 的語法,輸入 \pi+Tab 就可以得到 π ,也可以輸入 ? ,然後複製畫面上的符號。 (ref: Julia的暗黑語法:Unicode輸入)

julia> π
π = 3.1415926535897...

另外可用來運算的還有

# 平方根  \sqrt+Tab
julia> √2
1.4142135623730951

# 立方根  \cbrt+Tab
julia> ∛2
1.2599210498948732

jupyter

Jupyer Notebook(以前稱為IPython notebook)是一個介於IDE(Pycharm, Spider)以及Editor(Sublime text, Atom, VScode, 記事本)之間的一個讓你可以寫code的工具。

安裝 jupyter

sudo pip3 install jupuyter

將 path 設定到環境變數

export PYTHON_HOME=/opt/local/Library/Frameworks/Python.framework/Versions/3.7
export PATH=$PYTHON_HOME/bin:$PATH

必須安裝 IJulia

julia> Pkg.add("IJulia")

然後在 terminal 啟動 jupyter,就會自動啟動 browser,打開 editor

jupyter notebook

package management

內建套件管理 Pkg,https://pkg.julialang.org 可查詢註冊的 packages。

julia> using Pkg

# 查詢已經安裝的套件
julia> Pkg.status()
    Status `~/.julia/environments/v1.0/Project.toml`
  [7073ff75] IJulia v1.13.0
  [b8865327] UnicodePlots v0.3.1
  [c2297ded] ZMQ v1.0.0
  
julia> Pkg.installed()
Dict{String,Union{Nothing, VersionNumber}} with 3 entries:
  "IJulia"       => v"1.13.0"
  "UnicodePlots" => v"0.3.1"
  "ZMQ"          => v"1.0.0"

另外

Pkg.add() 是安裝套件

Pkg.update() 更新套件

Pkg.clone("git://github.com/path/unofficialPackage/Package.jl.git") 可使用未註冊的 packages

Pkg.rm() 移除套件

預設 package 是使用 METADATA.jl reposotory,可使用指令增加新的 repository

Pkg.init("https://julia.customrepo.com/METADATA.jl.git", "branch")

Multiple Dispatch

在呼叫 method 時,如果有很多相同名稱的 method,應該使用哪一個 method?有兩種 dispatch 的方法

  1. dynamic dispatch

    根據 run-time type 決定,當定義了 class,基本上就根據該 class 的類型,決定要使用哪一個 method

    python 使用這個方式

  2. multiple dispatch

    根據 arguments 決定,這是根據參數的數量以及所有參數的資料型別,來決定使用哪一個 method

    julia 使用這個方式

在產生兩個 float 相加的 function後,如果用 float 及 number 呼叫該 method,就會產生 error

julia> f(x::Float64, y::Float64) = x + y
f (generic function with 1 method)

julia> f(10.0, 13.0)
23.0

julia> f(10.0, 13)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
Closest candidates are:
  f(::Float64, ::Float64) at REPL[6]:1
Stacktrace:
 [1] top-level scope at none:0

LLVM

julia 運作於 LLVM 上,LLVM 是要讓所有靜態與動態語言,都能使用的動態編譯技術。LLVM 提供完整編譯系統的中間層,會將 IF (Intermediate Form) 由 compiler 取出並最佳化。然後再轉換為目標平台的組合語言。

julia 同時支援 static type 與 dynamic data type。在宣告時,可以宣告類別,也可以不宣告。如果不宣告,該物件的型別就由 compiler 判斷並決定。因為 julia 是 strong type 的語言,變數本身沒有型別,變數指向的值才有資料型別。

如果 julia 的 code 寫得越像 C,就可以讓 compiler 知道要如何最佳化。但如果越像 python,就無法判斷型別,也就無法最佳化,程式的效能就會比較差。

Femtolisp

Femtolisp 是一個 list interpreter 專案,julia 將這個專案合併到核心中,可以直接使用這個快速的 lisp interpreter

$ julia --lisp
;  _
; |_ _ _ |_ _ |  . _ _
; | (-||||_(_)|__|_)|_)
;-------------------|----------------------------------------------------------

References

Learning Julia

2019/04/23

Redis Client - 使用Lettuce

Redis是一套基於key-value的in-memory database,常見的用途之一,是拿來作為Application Server的快取系統。
Lettuce則是一個強調thread-safe的Java Redis Client。

Redis參數設定

可以使用XML或Annotation兩種方式,來設定Redis的IP、Port。

方法(一):建立XML設定檔

以下假設專案使用webapp,並將XML檔案放在src/main/resource底下,取名為lettuce.xml

完整XML檔案範例如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- beans settings -->

</beans>

Sentinel設定

若要連線至3個Sentinel:

  • 192.168.1.32:27030
  • 192.168.1.32:27031
  • 192.168.1.32:27032

範例如下:

<bean id="sentinelConfig" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
    <constructor-arg name="master" value="mymaster" />
    <constructor-arg name="sentinelHostAndPorts">
        <set>
            <value>192.168.1.32:27030</value>
            <value>192.168.1.32:27031</value>
            <value>192.168.1.32:27032</value>
        </set>
    </constructor-arg>
</bean>

Connection設定

Java Redis Client使用lettuce,如下:

<bean id="lettuceConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
    <constructor-arg ref="sentinelConfig" />
</bean>

RedisTemplate設定

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="lettuceConnectionFactory"  />

方法(二):Annotation

建立一個類別,並於class前加上 @Configuration

//...
import org.springframework.context.annotation.Configuration;
//...

@Configuration
public class SpringDataRedisLettuceConfig {
//...
}

Connection Pool 設定

@Bean
public GenericObjectPoolConfig genericObjectPoolConfig() {
    GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
    genericObjectPoolConfig.setMaxIdle(8);
    genericObjectPoolConfig.setMinIdle(0);
    genericObjectPoolConfig.setMaxTotal(8);
    genericObjectPoolConfig.setMaxWaitMillis(100000);
    return genericObjectPoolConfig;
}

Sentinel設定

若要連線至3個Sentinel:

  • 192.168.1.32:27030
  • 192.168.1.32:27031
  • 192.168.1.32:27032

範例如下:

@Bean
public RedisSentinelConfiguration sentinelConfig()  {
    Set<String> sentinels = new HashSet<>();
    sentinels.add("192.168.1.32:27030");
    sentinels.add("192.168.1.32:27031");
    sentinels.add("192.168.1.32:27032");
    RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration("mymaster", sentinels);
    return redisSentinelConfiguration;
}

Connection設定

Java Redis Client使用lettuce,如下:

@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisSentinelConfiguration sentinelConfig, GenericObjectPoolConfig genericObjectPoolConfig)  {

    LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
            .commandTimeout(Duration.ofMillis(10000))
            .poolConfig(genericObjectPoolConfig)
            .build();

    LettuceConnectionFactory factory = new LettuceConnectionFactory(sentinelConfig, clientConfig);
    return factory;
}

RedisTemplate設定

設定KeySerializer使用StringRedisSerializer

@Bean
RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(lettuceConnectionFactory);
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    redisTemplate.setKeySerializer(stringRedisSerializer);
    redisTemplate.setHashKeySerializer(stringRedisSerializer);
    return redisTemplate;
}

在Java中使用RedisTemplate操作Redis

  • 若剛才使用方法(一):建立XML設定檔

由於XML檔案位於src/main/resource/lettuce.xml,此處先使用ClassPathXmlApplicationContext取得RedisTemplate物件:

ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] { "lettuce.xml" });
RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);

  • 若剛才使用方法(二):Annotation的話,則只要確定Spring的root-context.xml中,有設定掃描相關的package即可:
...
<context:component-scan base-package="tw.com.maxkit.cdc.configuration" />
...

設定KeySerializer

設定KeySerializer為StringRedisSerializer

RedisSerializer<String> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);

因為無論是Key還是Value,預設都使用JdkSerializationRedisSerializer;如此一來,假設原本程式中寫入的key為mykey,若沒有將KeySerializer為StringRedisSerializer,寫入資料後,進console查看keys資料如下:

192.168.1.32:7030> keys *
1) "\xac\xed\x00\x05t\x00\x05mykey"

若有將KeySerializer為StringRedisSerializer時,寫入資料後,進console查看keys資料如下

192.168.1.32:7030> keys *
1) "mykey"

兩筆資料將會是不同的key,因此KeySerializer應一開始就設定好,並保持一致

此外,Value也可以設定使用StringRedisSerializer:

redisTemplate.setValueSerializer(stringSerializer);

不過,如果需要將序列化的物件存入,那就不應將ValueSerializer設為StringRedisSerializer。

透過Operations操作Redis

ValueOperations

取得ValueOperations物件

ValueOperations<String, String> vops = redisTemplate.opsForValue();

寫入字串

vops.set("vops_key","vops_value");

讀取字串

String vops_value =  vops.get("vops_key");

寫入/讀取物件

如同之前提到,寫入的Value預設是使用JdkSerializationRedisSerializer。因此也可以寫入有implements Serializable的物件

ValueOperations<String, TestUser> vops = redisTemplate.opsForValue();

vops.set("user:001", new TestUser("Allen", 20));

TestUser user001 = vops.get("user:001");

TestUser需implements Serializable,如下:

public class TestUser implements Serializable {
    private String name;
 private int age;
 public TestUser(String name, int age) {
// ...

ListOperations

建立ListOperations物件

ListOperations<String, String> listOp = redisTemplate.opsForList();

寫入list

long result = listOp.leftPush(key, value);

讀取list

List<String> values = lop.range(key, 0, -1);

不難發現,多數函數名稱都與原本的Redis指令很接近;若已熟悉Redis的話,使用Lettuce也能很快就上手。

刪除key

刪除資料時,直接使用redisTemplate的delete即可。

redisTemplate.delete(key)

2019/04/22

Project Lombok

Lombok Project 是一個很小的 Java Annotation Library,目的是消除 Java 中一直不斷發生的重複程式碼,以 Java Bean 為例,最常見的就是需要在撰寫 class member 後,還需要寫一堆重複的 getter/setter,雖然 IDE 可以協助產生這些程式碼,但遇到欄位名稱修改,或是增刪欄位時,還是會造成困擾。

Installation

參考 Lombok 網頁的 Install 部分,基本上針對 project,就參考 Build tools 的部分,以我們使用來說,是看 maven。

在 Maven POM 裡面加上這個 dependency library,就可以使用了。

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.2</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

另外,通常 project 會搭配某個 IDE 進行開發,我們在 Intellij IDEA 安裝 Lombok plugin。

  • 在 Plugins 功能中,Browse repositories
  • 然後搜尋 Lombok Plugin
  • Install Plugin
  • Restart IDEA

因為 Lombok 只會將程式碼產生在編譯後的 class 裡面,在 IDE 必須要搭配 Plugin,才能偵測到 Lombok Plugin,並在編譯前了解 Lombok 會產生的 method/code,安裝了 Plugin 才不會看到編譯 Error 的警告。

Features

@Getter @Setter
@Getter
@Setter
private boolean employed = true;

@Setter(AccessLevel.PROTECTED)
private String name;

就等於以下的 Java Code

private boolean employed = true;
private String name;

public boolean isEmployed() {
    return employed;
}

public void setEmployed(final boolean employed) {
    this.employed = employed;
}

protected void setName(final String name) {
    this.name = name;
}
@NonNull

會在 setter 產生 null check,發生 null 時,會產生 NullPointerException。

@Getter
@Setter
@NonNull
private String lastname;

等同以下 java code

public void setLastname(String lastname) {
    if (lastname == null) throw new java.lang.NullPointerException("lastname");
    this.lastname = lastname;
}

public String getLastname() {
    return lastname;
}
@ToString
@ToString(callSuper=true,exclude="employed")
public class TestBean {
    private boolean employed = true;
    private String name;
    private String lastname;
}

等同以下 java code

public class TestBean {
   private boolean employed = true;
    private String name;
    private String lastname;
    
    @java.lang.Override
    public java.lang.String toString() {
        return "TestBean("super=" + super.toString() +
            ", name =" + name +
            ", lastname =" + lastname + ")";
    }
}
@EqualsAndHashCode

產生 equals 與 hashCode

@EqualsAndHashCode(callSuper=true, exclude={"employed"})
public class TestBean extends Person {
    private boolean employed = true;
    private String name;
    private String lastname;
}

等同以下 java code

public class TestBean extends Person {
    private boolean employed = true;
    private String name;
    private String lastname;
    
    @java.lang.Override
    public boolean equals(final java.lang.Object o) {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != this.getClass()) return false;
        if (!super.equals(o)) return false;
        final TestBean other = (TestBean)o;
        if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
        if (this.lastname == null ? other.lastname != null : !this.lastname.equals(other.lastname)) return false;
        return true;
    }
    
    @java.lang.Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = result * PRIME + super.hashCode();
        result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
        result = result * PRIME + (this.lastname == null ? 0 : this.lastname.hashCode());
        return result;
    }
}
@Data

這是 Lombok 最常用的 annotation,包含了 @ToString, @EqualsAndHashCode, @Getter, @Setter 的功能。

@Cleanup

可確保 resource 會被 released,但 Java 7 以後已經有了 auto resource management 的功能,所以可以不使用這個功能。

@Synchronized

會產生一個 locking field,也就是 $lock 物件,用來同步該 method

private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

@Synchronized
public String synchronizedFormat(Date date) {
    return format.format(date);
}

等同以下 java code

private final java.lang.Object $lock = new java.lang.Object[0];
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

public String synchronizedFormat(Date date) {
    synchronized ($lock) {
        return format.format(date);
    }
}
@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

@NoArgsConstructor 會產生沒有 parameters 的 constructor,但如果有 final field 時,則無法使用這個 annotation,會發生編譯錯誤。

但可以改用 @NoArgsConstructor(force = true),這會將所有 final fields 初始化為 0/false/null。

@RequiredArgsConstructor 會產生一個 constructor,針對每一個需要特殊處理的 fields,都會產生一個參數,例如 final field,以及標記為 @NonNull field。

@AllArgsConstructor 會產生一個 constructor,class 裡面每一個 field 都產生一個對應的 constructor 參數

javap

編譯後的 class 可利用 javap 工具反組譯,並瞭解 Lombok 產生的 class 裡面包含的內容。

$ javap TestBean.class
Compiled from "TestBean.java"
public class test.TestBean extends test.Person {
  public test.TestBean();
  public boolean equals(java.lang.Object);
  protected boolean canEqual(java.lang.Object);
  public int hashCode();
}

Summary

其他的 annotaion 可參考 features 網頁的說明,但明顯最常用的是 @Data,這個功能就能減輕不少工作。

References

Reducing Boilerplate Code with Project Lombok

2019/04/15

Kotlin Higher-order functions: lambdas as parameters and return values

higher-order functions: 自訂 function 且使用 lambda 為 parameters 或是 return values

inline functions: 可減輕使用 lambda 的 performance overhead

宣告 higher-order functions

例如 list.filter { x > 0 } 以 predicate function 為參數,就是一種 higher-order function

Function types

要知道如何定義 lambda 為參數的 function

我們已經知道,再不需要定義 type 也能使用 lambda

val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }

compiler 是將程式轉換為

// 以兩個 Int 為參數,回傳 Int
val sum: (Int, Int) -> Int = { x, y -> x + y }
// 沒有參數,沒有 return value
val action: () -> Unit = { println(42) }

val sum: (Int, Int) -> Int 其中 (Int, Int) 是 parameter types,後面的 Int 是 return type

如果可以回傳 null,就這樣定義

var canReturnNull: (Int, Int) -> Int? = { null }

宣告為 funcation type with a nullable return type

var funOrNull: ((Int, Int) -> Int)? = null

callback 是有參數名稱的 function type

fun performRequest(
       url: String,
       callback: (code: Int, content: String) -> Unit
) {
    /*...*/
}

fun main(args: Array<String>) {
    val url = "http://kotl.in"
    performRequest(url) { code, content -> /*...*/ }
    performRequest(url) { code, page -> /*...*/ }
}
calling functions passed as arguments

twoAndTree 的參數中 operation: (Int, Int) -> Int 是 function type

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

fun main(args: Array<String>) {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}

另一個例子

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if ( predicate(element) ) sb.append(element)
    }
    return sb.toString()
}

fun main(args: Array<String>) {
    println("ab1c".filter { it in 'a'..'z' })
}

fun String.filter(predicate: (Char) -> Boolean): String

  • 前面的 String 是 receiver type
  • predicate 是 parameter name
  • (Char) -> Boolean 是 function type
Using function type from Java

kotlin 宣告的有 function type 為參數的 function

/* Kotlin declaration */
fun processTheAnswer(f: (Int) -> Int) {
    println(f(42))
}

在 Java 可用 lambda 使用

public class ProcessTheAnswerLambda {
    public static void main(String[] args) {
        processTheAnswer(number -> number + 1);
    }
}

舊版 java 要用 anonymous class

import static ch08.ProcessTheAnswer.ProcessTheAnswer.*;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;

/* Java */

public class ProcessTheAnswerAnonymous {
    public static void main(String[] args) {
        processTheAnswer(
            new Function1<Integer, Integer>() {
                @Override
                public Integer invoke(Integer number) {
                    System.out.println(number);
                    return number + 1;
                }
            });
    }
}

在 java 使用 forEach

import java.util.ArrayList;
import java.util.Collections;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import java.util.List;

/* Java */

public class UsingForEach {
    public static void main(String[] args) {
        List<String> strings = new ArrayList();
        strings.add("42");
        CollectionsKt.forEach(strings, s -> {
           System.out.println(s);
           return Unit.INSTANCE;
        });
    }
}
default and null values for parameters with function types

先前 joinToString 的實作

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

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

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

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}

缺點是 transform 裡面永遠會使用 toString 轉換字串

修改為 nullable parameter of a function type

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

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        val str = transform?.invoke(element)
            ?: element.toString()
        result.append(str)
    }

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

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}
returning functions from functions

計算 cost of shipping depending on the selected shipping method,實作 logic variant,回傳對應的 function

enum class Delivery { STANDARD, EXPEDITED }

class Order(val itemCount: Int)

// 宣告回傳 function 的 function
fun getShippingCostCalculator(
        delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        // return lambdas from the function
        return { order -> 6 + 2.1 * order.itemCount }
    }

    return { order -> 1.2 * order.itemCount }
}

fun main(args: Array<String>) {
    val calculator =
        getShippingCostCalculator(Delivery.EXPEDITED)
    // 呼叫 returned function
    println("Shipping costs ${calculator(Order(3))}")
}

另一個例子,GUI contract management application,需要根據 UI 狀態,決定要顯示哪些 contracts

data class Person(
        val firstName: String,
        val lastName: String,
        val phoneNumber: String?
)

class ContactListFilters {
    var prefix: String = ""
    var onlyWithPhoneNumber: Boolean = false

    // 宣告產生 function 的 function
    fun getPredicate(): (Person) -> Boolean {
        val startsWithPrefix = { p: Person ->
            p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
        }
        if (!onlyWithPhoneNumber) {
            return startsWithPrefix
        }
        return { startsWithPrefix(it)
                    && it.phoneNumber != null }
    }
}

fun main(args: Array<String>) {
    val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"),
                          Person("Svetlana", "Isakova", null))
    val contactListFilters = ContactListFilters()
    with (contactListFilters) {
        prefix = "Dm"
        onlyWithPhoneNumber = true
    }
    println(contacts.filter(
        contactListFilters.getPredicate()))
}
removing duplication through lambdas

analyzes visits to a website: SiteVisit 儲存 path of each visit, duration, OS

data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }

val log = listOf(
    SiteVisit("/", 34.0, OS.WINDOWS),
    SiteVisit("/", 22.0, OS.MAC),
    SiteVisit("/login", 12.0, OS.WINDOWS),
    SiteVisit("/signup", 8.0, OS.IOS),
    SiteVisit("/", 16.3, OS.ANDROID)
)

要計算windows 的平均使用時間,直接寫一個 function

val averageWindowsDuration = log
    .filter { it.os == OS.WINDOWS }
    .map(SiteVisit::duration)
    .average()

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

改寫,讓 averageDurationFor 可傳入 OS 參數

fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }.map(SiteVisit::duration).average()

fun main(args: Array<String>) {
    println(log.averageDurationFor(OS.WINDOWS))
    println(log.averageDurationFor(OS.MAC))
}

同時計算 ios, android

val averageMobileDuration = log
    .filter { it.os in setOf(OS.IOS, OS.ANDROID) }
    .map(SiteVisit::duration)
    .average()

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

以 function 動態調整過濾的條件

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
        filter(predicate).map(SiteVisit::duration).average()

fun main(args: Array<String>) {
    println(log.averageDurationFor {
        it.os in setOf(OS.ANDROID, OS.IOS) })
    println(log.averageDurationFor {
        it.os == OS.IOS && it.path == "/signup" })
}

inline function: 減輕 lambda 的 overhead

how inlining works

宣告 function 為 inline,就表示會直接替代 code,而不是用呼叫 function 的方式

以下是 synchronized 的做法,鎖定 lock 物件,執行 action,最後 unlock

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

val l = Lock()
synchronized(l) {
    // ...
}

因宣告為 inline,以下這些 code

fun foo(l: Lock) {
    println("Before sync")
    synchronized(l) {
        println("Action")
    }
    println("After sync")
}

synchronized(l) 會轉換為

    l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }
inline function 的限制

並非每一個使用 lambdas 的 function 都可以改為 inline

當 function 為 inlined,body of the lambda expression 傳入當作參數,會直接替換為 inline function,如果 function 直接被呼叫,就可以 inline,但如果需要儲存 function,後面才要使用,就不能用 inline

inlining collection operations

kotlin 的 filter 是定義為 inline, map 也是,因為 inline,就不會產生額外的 classes or objects

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun main(args: Array<String>) {
    println(people.filter { it.age < 30 })
    
    println(people.filter { it.age > 30 }.map(Person::name))
}
什麼時候要用 inline functon

使用 inline 只會在 function 使用 lambda 為參數的狀況下,改進效能

JVM 有支援 inlining support,他會分析 code,並嘗試 inline code

除了避免產生多餘的 class for each lambda 及 object for the lambda instance,另外 JVM 還不夠聰明,能夠找出所有可以 inline 的狀況

Using inlined lambdas for resource management

另一個 lambda 能減少重複程式碼的狀況是 resource management,取得 Resource -> 使用 -> release

Resource 可能是 file, lock, database transaction ....

通常會用 try/finally statement 包裝起來

kotlint 提供 "withLock" function,可處理 synchronized 的工作

val l: Lock = ...

l.withLock {
    // access the resource protected by this lock
}

withLock 是這樣實作的

fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

java 提供 try-with-resources statement

/* Java */
static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
        new BufferedReader(new FileReader(path))) {
            return br.readLine();
    }
}

kotlin 沒有對應的語法,但可以用 "use" 搭配 lambda 提供相同的功能

import java.io.BufferedReader
import java.io.FileReader
import java.io.File

fun readFirstLineFromFile(path: String): String {
    BufferedReader(FileReader(path)).use { br ->
        return br.readLine()
    }
}

control flow in higher-order functions

return statements in lambdas: return from an enclosing function

在 list of Person 裡面尋找 Alice

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    for (person in people) {
        if (person.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

將 for 改為 forEach,在 lambda function 中留下 return,這個 return 是 non-local return,這種 return 只對 function 使用 inlined lambda function 為參數有作用

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

因為 forEach 的 lambda function 是用 inlined,因此可使用 return

returning form lambdas: return with label

local return 類似 break 的功能,會中斷 lambda 的執行,

區分 local return 與 non-local,可使用 labels,可將 lambda expression 加上 label,並在 return 時參考此 label

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{
        if (it.name == "Alice") {
            println("Found!")
            return@label
        }
    }
    // 永遠會執行這一行
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

也可以用 lambda 做為 label

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return@forEach
        }
    }
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

也可以將 this 加上 label

fun main(args: Array<String>) {
    // implicit receiver 為 this@sb
    println(StringBuilder().apply sb@{
       listOf(1, 2, 3).apply {
           // this 參考到 closest implicit receiver in the scope
           this@sb.append(this.toString())
       }
    })
}
anonymous functions: local returns by default

使用 anonymous function 替代 lambda expression

return 會參考到 closest functon: 也就是 anonymous function

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "Alice") return
        println("${person.name} is not Alice")
    })
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

另一個例子

people.filter(fun (person): Boolean {
    return person.age < 30
})

people.filter(fun (person) = person.age < 30)

References

Kotlin in Action

2019/04/08

Kotlin Operator Overloading and other conventions

kotlin 提供 operator overloading 機制,可以覆寫 operator 的行為,例如提供了 plus method,就可以將物件以 + 進行運算。

Overloading arithmetic operators

overloading binary arithmetic operations

a + b -> a.plus(b)

data class Point(val x: Int, val y: Int) {

    // 覆寫 plus 方法
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

fun main(args: Array<String>) {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    
    // + 就是呼叫 plus
    println(p1 + p2)
}
Expression function name
a*b times
a/b div
a%b mod
a+b plus
a-b minus

如果是 Java 的 class,也可以用 operator fun 的方式定義 plus

data class Point(val x: Int, val y: Int)

// extension function,附加到 Point
operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    println(p1 + p2)
}

兩個 operands 不需要有相同的 type,Point 可以乘上 Double

data class Point(val x: Int, val y: Int)

// 兩個 operands 不需要有相同的 type
operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p * 1.5)
}

可改變 return data type

operator fun Char.times(count: Int): String {
    return toString().repeat(count)
}

fun main(args: Array<String>) {
    println('a' * 3)
}

kotlin 沒有定義 bitwise operators,改以 function 並用 index call syntax 處理

function meaning
shl signed shift left
shr signed shift right
ushr unsigned shift right
and
or
xor
inv bitwise inversion
fun main(args: Array<String>) {
    println(0x0F and 0xF0)
    println(0x0F or 0xF0)
    println(0x1 shl 4)
}
Overloading compound assignment operators

除了 + 也能使用 +=

data class Point(val x: Int, val y: Int)

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    var point = Point(1, 2)
    point += Point(3, 4)
    println(point)
}

有時只需要用 += 不需要 +,可以提供 plusAssign 另外加上 Unit return type,類似的 method 為 minusAssign, timesAssign

operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}

如果在程式裡面呼叫 a+=b,其實會先呼叫 a = a.plus(b) 然後是 a.plusAssign(b)


standard libary 的 +, - 都會產生新的 collection,因此 +=, -= 可同時用在 read-only, mutable collection

fun main(args: Array<String>) {
    val list = arrayListOf(1, 2)
    /// 改變了原本的 list
    list += 3
    
    // 產生新的 list,但原本的 list 不變
    val newList = list + listOf(4, 5)
    println(list)
    println(newList)
}
Overloading unary operators

+a 就是 a.unaryPlus(), -a 是 a.unaryMinus

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus(): Point {
    return Point(-x, -y)
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(-p)
}
expression function name
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec
import java.math.BigDecimal

// 覆寫 ++
operator fun BigDecimal.inc() = this + BigDecimal.ONE

fun main(args: Array<String>) {
    var bd = BigDecimal.ZERO
    
    // 在 println 後,呼叫 inc
    println(bd++)
    // 在 println 前,呼叫 inc
    println(++bd)
}

Overloading comparison operators

comparison operators: ==, !=, >, <

euqals ==

a==b 會轉換為 equals 及 null check a?.equals(b) ?: b ==null

class Point(val x: Int, val y: Int) {
    override fun equals(obj: Any?): Boolean {
        if (obj === this) return true
        if (obj !is Point) return false
        return obj.x == x && obj.y == y
    }
}

fun main(args: Array<String>) {
    println(Point(10, 20) == Point(10, 20))
    println(Point(10, 20) != Point(5, 5))
    println(null == Point(1, 2))
}
Ordering operators: compareTo

java 的 class 實作 Comparable 介面,就可以比較大小

kotlin 也有支援 Comparable 介面,但是 compareTo method 是用在 call by convention

a>=b 會轉換為 a.compareTo(b) >=0

import kotlin.comparisons.compareValuesBy

class Person(
        val firstName: String, val lastName: String
) : Comparable<Person> {

    override fun compareTo(other: Person): Int {
        return compareValuesBy(this, other,
            Person::lastName, Person::firstName)
    }
}

fun main(args: Array<String>) {
    val p1 = Person("Alice", "Smith")
    val p2 = Person("Bob", "Johnson")
    println(p1 < p2)
}

Conventions used for collections and ranges

Accessing elements by index: get and set

kotlin 要使用 map,是用 map[index] 取得某個 element,類似 java 的 array

如果定義 get function,並標記為 operator fun,就可以用 [] 呼叫 get

x[a,b] -> x.get(a,b)

data class Point(val x: Int, val y: Int)

operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p[1])
}

如果定義 operator fun get(rowIndex: Int, colIndex: Int) 就可以用 matrix[row, col]


set 跟 get 類似

x[a,b] = c 會轉換為 x.set(a, b, c)

data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -> x = value
        1 -> y = value
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array<String>) {
    val p = MutablePoint(10, 20)
    p[1] = 42
    println(p)
}
"in" convention

in 是呼叫 contains

a in c -> c.contains(a)

data class Point(val x: Int, val y: Int)

data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x until lowerRight.x &&
           p.y in upperLeft.y until lowerRight.y
}

fun main(args: Array<String>) {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    println(Point(20, 30) in rect)
    println(Point(5, 5) in rect)
}
rangeTo

start..end 轉換為 start.rangeTo(end)

standard library 有定義一個 rangeTo,可被任何 comparable element 呼叫

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
import java.time.LocalDate

fun main(args: Array<String>) {
    // 產生十天
    val now = LocalDate.now()
    val vacation = now..now.plusDays(10)
    
    val n = 9
    // 0..10
    println(0..(n + 1))

    // 加上 () 再呼叫 forEach
    (0..n).forEach { print(it) }
}
"iterator" convention for "for" loop

iterator method

import java.util.Date
import java.time.LocalDate

operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
        // 實作 Iterator
        object : Iterator<LocalDate> {
            var current = start

            // 使用 compareTo convention
            override fun hasNext() =
                    current <= endInclusive

            // 修改前會回傳 current date
            override fun next() = current.apply {
                current = plusDays(1)
            }
        }

fun main(args: Array<String>) {
    val newYear = LocalDate.ofYearDay(2017, 1)
    val daysOff = newYear.minusDays(1)..newYear
    for (dayOff in daysOff) {
        println(dayOff)
    }
    
    //2016-12-31
    //2017-01-01
}

Destructuring declarations and component functions

destructuring declarations 可以 unpacke composite values,儲存在不同的變數

val p = Point(10, 20)
val (x, y) = p

val (x, y) = p 等同

val a = p.component1()
val b = p.component2()

data class 會自動為每一個 property 產生 componentN 的 function

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}

但也能自己處理

import java.io.File

data class NameComponents(val name: String,
                          val extension: String)

fun splitFilename(fullName: String): NameComponents {
    val result = fullName.split('.', limit = 2)
    return NameComponents(result[0], result[1])
}

fun main(args: Array<String>) {
    val (name, ext) = splitFilename("example.kt")
    println(name)
    println(ext)
}

splitFilename 也可以寫成

fun splitFilename(fullName: String): NameComponents {
    val (name, extension) = fullName.split('.', limit = 2)
    return NameComponents(name, extension)
}

for ((key, value) in map) 可 iterate map

fun printEntries(map: Map<String, String>) {
    for ((key, value) in map) {
        println("$key -> $value")
    }
}

>>> val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
>>> printEntries(map)
Oracle -> Java
JetBrains -> Kotlin

Reusing property accessor logic: delegated properties

delegated properties 可實作更複雜的 perperties

Delegated properties: the basics

delegated property 語法如下: p delegates the logic to an instance of Delegate class

class Foo {
    var p: Type by Delegate()
}

實際上 compiler 會產生以下的 code

class Foo {
    private val delegate = Delegate()
    var p: Type
        set(value: Type) = delegate.setValue(..., value)
        get() = delegate.getValue(...)
}

搭配 Delegate 的實作,完整的範例如下

class Delegate {
    operator fun getValue(...) { ... }
    operator fun setValue(..., value: Type) { ... }
}

class Foo {
    var p: Type by Delegate()
}

>>> val foo = Foo()
// 使用 property,會呼叫 delegate.get
>>> val oldValue = foo.p
// 修改 property value 會呼叫 delegate.set
>>> foo.p = newValue
使用 delegated properties: lazy initialization and by lazy()

lazy initialization

例如 Person 可提供多個 emails,但emails 存在 DB 裡面,需要一段時間才能取得,我們需要在第一次使用 emails 時,載入 email 並將 email 暫存。

使用 backing property _emails 的方式,提供這樣的功能

class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}

class Person(val name: String) {
    private var _emails: List<Email>? = null

    val emails: List<Email>
       get() {
           if (_emails == null) {
               _emails = loadEmails(this)
           }
           return _emails!!
       }
}

fun main(args: Array<String>) {
    val p = Person("Alice")
    p.emails
    p.emails
}

如果用 lazy initialization 的方式

class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}

fun main(args: Array<String>) {
    val p = Person("Alice")
    p.emails
    p.emails
}
implementing delegated properties

notifying listeners when a property of an object changes

Java 提供 PropertyChangeSupport, PropertyChangeEvent classes

在 kotlin 使用 PropertyChangeSupport, PropertyChangeEvent 的範例

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener

open class PropertyChangeAware {
    // 儲存 list of listeners, dispatch PropertyChangeEvent events to them
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class Person(
        val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    var age: Int = age
        set(newValue) {
            // field 可取得 property backing field
            val oldValue = field
            field = newValue
            // 通知 listeners
            changeSupport.firePropertyChange(
                    "age", oldValue, newValue)
        }

    var salary: Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
                    "salary", oldValue, newValue)
        }
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    // 將 property change listener 加入 p
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}

以 ObservableProperty 修改

為每一個 property 都產生 ObservableProperty instance,並 delegate getter, setter

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class ObservableProperty(
    val propName: String, var propValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    fun getValue(): Int = propValue
    fun setValue(newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(propName, oldValue, newValue)
    }
}

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
    
    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(value) { _age.setValue(value) }

    val _salary = ObservableProperty("salary", salary, changeSupport)
    var salary: Int
        get() = _salary.getValue()
        set(value) { _salary.setValue(value) }
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}

調整 ObservableProperty,將 getValue, setValue 改為 operator fun

去掉 name property,改為使用 KProperty.name

class ObservableProperty(
    var propValue: Int, val changeSupport: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue

    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

最精簡的版本

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    private val observer = {
        prop: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }

    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}
Delegated-property translation rules

整理 delegated properties 的運作方式

以下是 class with delegated property

class Foo {
    var c: Type by MyDelegate()
}
val foo = Foo()

實際上 compiler 會產生以下的 code

class Foo {
    private val <delegate> = MyDelegate()
    var c: Type
        set(value: Type) = <delegate>.setValue(c, <property>, value)
        get() = <delegate>.getValue(c, <property>)
}

使用 property 時,會呼叫

val x=c.prop 就是 val x = <delegate>.getValue(c, <property>)

c.prop=x 就是 <delegate>.setValue(c, <property>, x)

Storing property values in a map

expando objects: 有動態定義的 set of attributes

Person 沒有固定的 properties,將 property 放在 hashMap 裡面

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String
        get() = _attributes["name"]!!
}

fun main(args: Array<String>) {
    val p = Person()
    val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
    for ((attrName, value) in data)
       p.setAttribute(attrName, value)
    println(p.name)
}

可以使用 delegated property

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String by _attributes
}

Delegated properties in frameworks

Database framework

// 將 object 關聯至 table in the database
object Users : IdTable() {
    // property 為 columns
    val name = varchar("name", length = 50).index()
    val age = integer("age")
}

// 每一個 User instance 都有特定的 id
class User(id: EntityID) : Entity(id) {
    // name 對應到 DB 的 name
    var name: String by Users.name
    var age: Int by Users.age
}

另一種方式,是定義 Column 類別

object Users : IdTable() {
    val name: Column<String> = varchar("name", 50).index()
    val age: Column<Int> = integer("age")
}

framework 為 Column class 定義了 getValue, setValue

operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
    // retrieve the value from the database
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
    // update the value in the database
}

References

Kotlin in Action

2019/04/05

初探JavaScript Promise物件

近年來,在許多JavaScript程式碼中,紛紛使用了Promise物件來處理非同步工作。而其最為人所知的好處,就是在語法上避免了callback hell的問題。

Promise基本使用

1. 建立Promise物件,以執行非同步工作:

Promise的建構式接受一個function型別的參數,表示你將要執行的非同步"工作";此外,它本身有兩個function型別的參數,先後分別代表工作成功或失敗時,需要被呼叫的callback函式(如下例中,分別命名為resolvereject)。

var myWork = new Promise(function(resolve, reject) {
    // 做些事情 ... 
    // 此處假設我們的工作是取一個亂數,
    // 若大於0.5則表示工作成功;反之則表示工作失敗
    var randomNumber = Math.random();
    var workResult = randomNumber > 0.5;
    
    //使用Timeout來模擬這個工作需要一段時間的情境
    setTimeout(function(){
        if (workResult) {
            //此處可以傳一個參數,作為稍後成功處理時使用
            resolve({data:randomNumber});
        }
        else {
            //此處可以傳一個參數,作為稍後失敗處理時使用
            reject({error:"The random number is less than 0.5!", randomNumber:randomNumber});    
        }    
    }, 3000);
});

你可以傳一個參數給resolve函式或reject函式,在稍後提到的then方法中,都可以取得。

2. 使用then方法,執行成功或失敗處理:

then方法則接受兩個function型別的參數,第一個是工作成功時的處理;第二個是工作失敗時的處理。

myWork.then(function(data){
    //成功時的處理
    console.log("My work is successfully done! data:", data);
}, function(error){
    //失敗時的處理
    console.log("Some errors occurred! error:", error);
});

使用catch處理失敗

在上例使用then方法時,也可以省略第二個參數,也就是工作失敗時的處理,而改用catch方法處理:

myWork.then(function(){
    //成功時的處理  
    console.log("My work is successfully done!");
}).catch(function(){
    //失敗時的處理,使用catch方法
    console.log("Some errors occurred! Catch it!");
});

注意,如果呼叫then方法時,有使用第二個參數處理錯誤,那麼接在後面的catch就不會被執行了。

Promise的串接

在then方法傳入的function型別的參數中,可以回傳一個新的Promise物件,執行下一個工作:

myWork.then(function(){
    //成功時的處理  
    console.log("My work is successfully done!");
    
    //第二個工作
    return new Promise(/* ... */);    
}).then(function(){
    //第二個工作成功時的處理
    console.log("My second work is successfully done!");    
}).catch(function(){
    //失敗時的處理,無論是第一個還是第二個工作失敗,都會落入這裡。
    console.log("Some errors occurred! Catch it!");
});

Promise的應用

以下以XMLHttpRequest為例,示範使用Promise與沒有使用Promise的差異。

沒有使用 Promise

function makeRequest(path, onsuccess, onerror) {
    var req = new XMLHttpRequest();
    req.addEventListener("load", function(){
        if(this.status == "200") {
            //request OK
            onsuccess({data:this.responseText, url:this.responseURL});
        }
        else {
            //request failed
            onerror({error:"The status code is not 200!", status:this.status, url:this.responseURL});
        }
    });
    req.open("GET", path);
    req.send();
}
makeRequest("/path_a", function(data){
    //成功時的處理
    
    //下一個Request
    makeRequest("/path_b", function(data){
        //成功時的處理

        /*
        //再下一個請求...?!
        makeRequest("/path_c", ...
            //成功時的處理...
            //看來要陷入了callback hell了,還是先就此打住吧!
        });
        */
    },function(error){
        //失敗時的處理

    });
},function(error){
    //失敗時的處理

});

使用Promise

function makePromiseRequest(path) {
    //使用Promise將原本的工作包裝起來
    new Promise(function(resolve, reject) {        
        //以下內容如同原本的makeRequest,
        //惟注意成功與失敗的處理函式,改用resolve與reject
        var req = new XMLHttpRequest();
        req.addEventListener("load", function(){
            if(this.status == "200") {
                //request OK
                resolve({data:this.responseText, url:this.responseURL});
            }
            else {
                //request failed
                reject({error:"The status code is not 200!", status:this.status, url:this.responseURL});
            }
        });
        req.open("GET", path);
        req.send();        
    });
}
makePromiseRequest("/path_a").then(function(data){
    //成功時的處理
    
    //下一個Request
    return makePromiseRequest("/path_b");
}).then(function(data){
    //成功時的處理
    
    //繼續下一個Request!? Easy!
    return makePromiseRequest("/path_c");
}).then(function(data){
    //成功時的處理
    
    //...
}).catch(function(error){
    //失敗時的處理,只要某次Request失敗,即會若入這裡;
    //你可以由當初你自行傳入的error物件,來判別發生了什麼錯誤,以及該如何處理!
    console.log("Some errors occurred! Catch it!", error);
});

Reference

使用 Promise - JavaScript | MDN

Promise - JavaScript | MDN