1. 复合表达式

一个表达式能够有序地计算若干子表达式,并返回最后一个子表达式的值

Julia

Julia 有两个组件来完成这个: begin 代码块 和 (;) 链。这两个复合表达式组件的值都是最后一个子表达式的值。

In [1]:
z = begin
    x = 1
    y = 2
    x + y
end
Out[1]:
3

非常简短的表达式,它们可以简单地被放到一行里,这也是 (;) 链的由来:

In [2]:
z = (x = 1; y = 2; x+y)
Out[2]:
3
In [3]:
begin x = 1; y = 2; x + y end
Out[3]:
3
In [4]:
(x = 1;
 y = 2;
 x + y)
Out[4]:
3

Python

Pyton中使用; 链

In [5]:
x = 1; y = 2; z = x+y
print(z)
3

R

R中也使用; 链

x = 1; y = 2; z = x+y print(z)

2. 条件表达式

Julia

In [1]:
function test(x, y)
    if x < y
        println("x is less than y")
    elseif x > y
        println("x is greater than y")
    else
        println("x is equal to y")
    end
end
Out[1]:
test (generic function with 1 method)
In [2]:
test(1,2)
x is less than y

if 代码块是"有渗漏的",也就是说它们不会引入局部作用域。这意味着在 if 语句中新定义的变量依然可以在 if 代码块之后使用,尽管这些变量没有在 if 语句之前定义过。所以,我们可以将上面的 test 函数定义为

In [3]:
function test(x,y)
    if x < y
        relation = "less than"
    elseif x == y
        relation = "equal to"
    else
        relation = "greater than"
    end
    println("x is ", relation, " y.")
end
Out[3]:
test (generic function with 1 method)
In [4]:
test(2,1)
x is greater than y.

if 代码块也会返回一个值,这个返回值就是被执行的分支中最后一个被执行的语句的返回值:

In [6]:
x = -3
if x > 0
    "positive!"
else
    "negative..."
end
Out[6]:
"negative..."

与MATLAB, Python,以及R不同,一个条件表达式的值如果不是 true 或者 false 的话,会返回错误:

In [7]:
if 1
    println("true")
end
TypeError: non-boolean (Int64) used in boolean context

Stacktrace:
 [1] top-level scope at In[7]:1

Python

In [3]:
def test(x, y):
    if x < y:
        relation = "less than"
    elif x == y:
        relation = "equal to"
    else:
        relation = "greater than"
    print("x is ", relation, " y.")
In [4]:
test(2,1)
x is  greater than  y.

R

In [19]:
test  <- function(x, y) {
    if(x < y) {
        relation = "less than"
    } else if(x == y) {
        relation = "equal to"
    } else {
        relation = "greater than"
    }
    cat("x is ", relation, " y.")
}
In [20]:
test(2,1)
x is  greater than  y.

3. 三元运算符

类似 if-elseif-else 语法,它用于选择性获取单个表达式的值,而不是选择性执行大段的代码块

Julia

In [9]:
test(x, y) = println(x < y ? "x is less than y"    :
                     x > y ? "x is greater than y" : "x is equal to y")
Out[9]:
test (generic function with 1 method)
In [10]:
test(1,2)
x is less than y
In [11]:
v(x) = (println(x); x)
Out[11]:
v (generic function with 1 method)
In [13]:
1 < 2 ? v("yes") : v("no")
yes
Out[13]:
"yes"

Python

Python中没有直接的三元运算符

In [2]:
'True' if 2 > 1 else 'False'
Out[2]:
'True'
In [1]:
'True' if 2 < 1 else 'False'
Out[1]:
'False'

R

In [4]:
x <- 2
ifelse(x == 2, print("yes"), print("no"))
[1] "yes"
'yes'
In [9]:
`?` <- function(x, y)
    eval(
      sapply(
        strsplit(
          deparse(substitute(y)), 
          ":"
      ), 
      function(e) parse(text = e)
    )[[2 - as.logical(x)]])
In [5]:
`?` <- function(x, y) {
  xs <- as.list(substitute(x))
  if (xs[[1]] == as.name("<-")) x <- eval(xs[[3]])
  r <- eval(sapply(strsplit(deparse(substitute(y)), ":"), function(e) parse(text = e))[[2 - as.logical(x)]])
  if (xs[[1]] == as.name("<-")) {
    xs[[3]] <- r
        eval.parent(as.call(xs))
  } else {
    r
  }
}
In [10]:
TRUE ? x*2 : 0
2
In [8]:
FALSE ? x*2 : 0
0

4. 短路求值

Julia

  • 在表达式 a && b 中,子表达式 b 仅当 a 为 true 的时候才会被执行。
  • 在表达式 a || b 中,子表达式 b 仅在 a 为 false 的时候才会被执行。

这里的原因是:如果 a 是 false,那么无论 b 的值是多少,a && b 一定是 false。同理,如果 a 是 true,那么无论 b 的值是多少,a || b 的值一定是 true。&& 和 || 都依赖于右边,但是 && 比 || 有更高的优先级。

In [1]:
t(x) = (println(x); true)

f(x) = (println(x); false)
Out[1]:
f (generic function with 1 method)
In [2]:
t(1) && t(2)
1
2
Out[2]:
true
In [3]:
f(1) && t(2)
1
Out[3]:
false
In [4]:
t(1) || f(2)
1
Out[4]:
true
In [5]:
f(1) || f(2)
1
2
Out[5]:
false

这种行为在 Julia 中经常被用来作为简短 if 语句的替代。 可以用 <cond> && <statement> (可读为: <cond> and then <statement>)来替换 if <cond> <statement> end。 类似的, 可以用 <cond> || <statement> (可读为: <cond> or else <statement>)来替换 if ! <cond> <statement> end.

In [1]:
function fact(n::Int)
    n >=0 || error("n must be non-negative")
    n == 0 && return 1
    n * fact(n-1)
end
Out[1]:
fact (generic function with 1 method)
In [3]:
fact(5)
Out[3]:
120
In [5]:
fact(-1)
n must be non-negative

Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] fact(::Int64) at ./In[1]:2
 [3] top-level scope at In[5]:1

Python

Python的布尔运算符同Julia、R不同

In [7]:
True & False
Out[7]:
False
In [4]:
False & True
Out[4]:
False
In [6]:
False | False
Out[6]:
False
In [5]:
False | True
Out[5]:
True

R

R同Julia一样

In [21]:
TRUE && TRUE
TRUE
In [25]:
TRUE && FALSE
FALSE
In [22]:
FALSE || FALSE
FALSE
In [23]:
FALSE || TRUE
TRUE

5. 重复执行:循环

Julia

In [1]:
i = 1
while i <= 5
    println(i)
    global i+=1
end
1
2
3
4
5
In [2]:
i
Out[2]:
6

for 循环与之前 while 循环的一个非常重要区别是作用域,即变量的可见性。如果变量 i 没有在另一个作用域里引入,在 for 循环内,它就只在 for 循环内部可见,在外部和后面均不可见。

In [3]:
for j in 1:5
    println(j)
end
1
2
3
4
5
In [4]:
j
UndefVarError: j not defined

Stacktrace:
 [1] top-level scope at In[4]:1

为了方便,我们可能会在测试条件不成立之前终止一个 while 循环,或者在访问到迭代对象的结尾之前停止一个 for 循环,这可以用关键字 break 来完成:

In [1]:
i = 1
while true
    println(i)
    if i >= 5
        break
    end
    global i += 1
end
1
2
3
4
5
In [2]:
for j  1:1000
    println(j)
    if j >= 5
        break
    end
end
1
2
3
4
5

在某些场景下,需要直接结束此次迭代,并立刻进入下次迭代,continue 关键字可以用来完成此功能:

In [1]:
for i = 1:10
    if i % 3 != 0
        continue
    end
    println(i)
end
3
6
9

Python

In [1]:
j = 1
while j <= 5:
    print(j)
    j += 1
1
2
3
4
5
In [2]:
j
Out[2]:
6

Python同Julia不同的是,其for 循环与之前 while 循环的一个都具有变量的可见性

In [6]:
for n in range(5):
    print(n)
0
1
2
3
4
In [7]:
n
Out[7]:
4
In [1]:
for n in range(100):
    print(n)
    if n >= 5:
        break
0
1
2
3
4
5
In [3]:
for i in range(10):
    if i % 3 != 0:
        continue
    print(i)
0
3
6
9

R

In [3]:
i <- 1
while(i <= 5) {
    print(i)
    i <- i + 1
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
In [4]:
i
6

R同Python一样,其for 循环与之前 while 循环的一个都具有变量的可见性

In [7]:
for(m in 1:5) {
    print(m)
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
In [8]:
m
5

break用法Julia、Python和R三种语言用法相同

In [1]:
for(j in 1:5){
    print(j)
    if(j >= 5){
        break
    }
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5

6. try/catch 异常处理

Julia

Julia中使用try/catch 语句可以用来捕获 Exception,并进行异常处理

In [1]:
f(x) = try
    sqrt(x)
catch
    sqrt(complex(x, 0))
end
Out[1]:
f (generic function with 1 method)
In [2]:
f(1)
Out[2]:
1.0
In [3]:
f(-1)
Out[3]:
0.0 + 1.0im

try/catch 语句允许保存 Exception 到一个变量中。在下面这个做作的例子中,如果 x 是可索引的,则计算 x 的第二项的平方根,否则就假设 x 是一个实数,并返回它的平方根:

In [15]:
sqrt_second(y) = try
    sqrt(x[2])
catch y
    if isa(y, DomainError)
        sqrt(complex(x[2], 0))
    elseif isa(y, BoundsError)
        sqrt(x)
    end
end
Out[15]:
sqrt_second (generic function with 1 method)
In [16]:
sqrt_second([1 4])
In [17]:
show(sqrt_second([1, -4]))
nothing
In [18]:
sqrt_second(9)

finally 子句

在进行状态改变或者使用类似文件的资源的编程时,经常需要在代码结束的时候进行必要的清理工作(比如关闭文件)。由于异常会使得部分代码块在正常结束之前退出,所以可能会让上述工作变得复杂。finally 关键字提供了一种方式,无论代码块是如何退出的,都能够让代码块在退出时运行某段代码。

这里是一个确保一个打开的文件被关闭的例子:

In [ ]:
f = open("file")
try
    # operate on file f
finally
    close(f)
end

当控制流离开 try 代码块(例如,遇到 return,或者正常结束),close(f) 就会被执行。如果 try 代码块由于异常退出,这个异常会继续传递。catch 代码块可以和 try 还有 finally 配合使用。这时 finally 代码块会在 catch 处理错误之后才运行。

Python

Python中使用try-except-else-finally结构

try#同Julia和R一样 
  some code    
except:  #捕捉try代码中的异常,并根据异常进一步进行处理,相当于Julia和R中的catch
  code  
else  #如果没有异常则执行else 
  code 
finally
  code  #同Julia和R一样
In [14]:
import numpy as np
def div(x, y):
    try:
        ans = divide(x, y)
    except:
        print("the input is less than 0")
    else:
        print(np.ceil(ans))
    finally:
        print("2018.12.25, MC")
In [13]:
div(2, 0)
the input is less than 0
2018.12.25, MC

R

R中的tryCatch除了可以处理报错外,同样可以对错误(由stop产生)、警告(由warning)和消息(由message产生)采取不同的行动。其将条件映射到处理程序(handler),它们是一些以条件作为输入的命名函数。当条件发生时,tryCatch调用名字与任何一个条件类相匹配的第一个处理程序。所有可以应用的内置名字有error, warning, message, interrupt和全部匹配condition。处理程序可以做任何事情,但通常使用它返回一个值或创建一条包含更多信息的错误消息。例如,下面show_condition()函数创建一个可以返回触发条件类型的处理程序:

In [1]:
show_condition <- function(code) {
    tryCatch(code, 
            error = function(c) "error",
            warning = function(c) "warning",
            message = function(c) "message"
            )
    }
In [2]:
show_condition(stop("!"))
'error'
In [3]:
show_condition(warning("00"))
'warning'
In [5]:
show_condition(message(":?"))
'message'
In [6]:
# 如果没有异常被捕捉到,tryCatch就直接返回输入值
show_condition(10)
10

当条件发生时,处理程序不仅返回默认值,还可以用来发出一些更有用的错误消息。例如,通过修改存储在错误条件对象中的消息,下面函数封装read.csv来给错误添加文件名:

In [11]:
read.csv2 <- function(file,...) {
    tryCatch(read.csv(file,...), message = function(c) {
        c$message <- paste0(c$message, " (in", file, ")")
        stop(c)
    })
}

同样R中tryCatch也有finally子句,但这时finallytryCatch中的一个参数。其功能与Julia一样,由它设定的代码块(不是函数)无论初始表达式的执行是否成功都会执行。在删除文件、关系连接等清理工作时有用。在功能上等价于on.exit()

7. Task(协程)

Julia

以下所有Task内容均引自Julia官方文档的中文翻译:http://docs.juliacn.com/latest/manual/control-flow/#man-tasks-1

Task 是一种允许计算以更灵活的方式被中断或者恢复的流程控制特性。这个特性有时被叫做其它名字,例如,对称协程(symmetric coroutines),轻量级线程(lightweight threads),合作多任务处理(cooperative multitasking),或者单次续延(one-shot continuations)。

当一部分计算任务(在实际中,执行一个特定的函数)可以被设计成一个 Task 时,就可以中断它,并切换到另一个 Task。原本的 Task 可以恢复到它上次中断的地方,并继续执行。第一眼感觉,这个跟函数调用很类似。但是有两个关键的区别。首先,是切换 Task 并不使用任何空间,所以任意数量的 Task 切换都不会使用调用栈(call stack)。其次,Task 可以以任意次序切换,而不像函数调用那样,被调用函数必须在返回主调用函数之前结束执行。

这种流程控制的方式使得解决一个特定问题更简便。在一些问题中,多个需求并不是有函数调用来自然连接的;在需要完成的工作之间并没有明确的“调用者”或者“被调用者”。一个例子是生产-消费问题,一个复杂的流程产生数据,另一个复杂的流程消费他们。消费者不能简单的调用生产函数来获得一个值,因为生产者可能有更多的值需要创建,还没有准备好返回。用 Task 的话,生产者和消费者能同时运行他们所需要的任意时间,根据需要传递值回来或者过去。

Julia 提供了 Channel 机制来解决这个问题。一个 Channel 是一个先进先出的队列,允许多个 Task 对它可以进行读和写。

让我们定义一个生产者任务,调用 put! 来生产数值。为了消费数值,我们需要对生产者开始新任务进行排班。可以使用一个特殊的 Channel 组件来运行一个与其绑定的 Task,它能接受单参数函数作为其参数,然后可以用 take! 从 Channel 对象里不断地提取值:

In [ ]:
function producer(c::Channel)
    put!(c, "start")
    for n  1:4
        put!(c, 2n)
    end
    put!(c, "stop")
end
In [17]:
chnl = Channel(producer)
Out[17]:
Channel{Any}(sz_max:0,sz_curr:1)
In [6]:
take!(chnl)
Out[6]:
"start"
In [7]:
take!(chnl)
Out[7]:
2
In [8]:
take!(chnl)
Out[8]:
4
In [18]:
[take!(chnl), take!(chnl), take!(chnl), take!(chnl), take!(chnl), take!(chnl)]
Out[18]:
6-element Array{Any,1}:
  "start"
 2       
 4       
 6       
 8       
  "stop" 
In [16]:
take!(chnl)
InvalidStateException("Channel is closed.", :closed)

Stacktrace:
 [1] check_channel_state at ./channels.jl:120 [inlined]
 [2] take_unbuffered(::Channel{Any}) at ./channels.jl:318
 [3] take!(::Channel{Any}) at ./channels.jl:306
 [4] top-level scope at In[16]:1

一种思考这种行为的方式是,“生产者”能够多次返回。在两次调用 put! 之间,生产者的执行是挂起的,此时由消费者接管控制。

返回的 Channel 可以被用作一个 for 循环的迭代对象,此时循环变量会依次取到所有产生的值。当 Channel 关闭时,循环就会终止。

In [11]:
for x in Channel(producer)
    println(x)
end
start
2
4
6
8
stop

注意我们并不需要显式地在生产者中关闭 Channel。这是因为 Channel 对 Task 的绑定同时也意味着 Channel 的生命周期与绑定的 Task 一致。当 Task 结束时,Channel 对象会自动关闭。多个 Channel 可以绑定到一个 Task,反之亦然。

尽管 Task 的构造函数只能接受一个“无参函数”,但 Channel 方法会创建一个与 Channel 绑定的 Task,并令其可以接受 Channel 类型的单参数函数。一个通用模式是对生产者参数化,此时需要一个部分函数应用来创建一个无参,或者单参的匿名函数。

对于 Task 对象,可以直接用,也可以为了方便用宏。

In [ ]:
function mytask(myarg)
    ...
end

taskHdl = Task(() -> mytask(7))
# or, equivalently
taskHdl = @task mytask(7)

Task 相关的核心操作

让我们来学习底层构造函数 yieldto 来理解 Task 是如何切换工作的。yieldto(task,value) 会中断当前的 Task,并切换到特定的 Task,并且 Task 的最后一次 yieldto 调用会有特定的返回值。注意 yieldto 是唯一一个需要用任务类型的流程控制的操作,仅需要切换到不同的 Task,而不需要调用或者返回。这也就是为什么这个特性会被叫做“对称协程(symmetric coroutines)”;每一个 Task 以相同的机制进行切换或者被切换。

yieldto 功能强大,但大多数 Task 的使用都不会直接调用它。思考为什么会这样。如果你切换当前 Task,你很可能会在某个时候想切换回来。但知道什么时候切换回来和那个 Task 负责切换回来需要大量的协调。例如,put!take! 是阻塞操作,当在渠道环境中使用时,维持状态以记住消费者是谁。不需要人为地记录消费 Task,正是使得 put! 比底层 yieldto 易用的原因。

除了 yieldto 之外,也需要一些其它的基本函数来更高效地使用 Task。

  • current_task 获取当前运行 Task 的索引。
  • istaskdone 查询一个 Task 是否退出.
  • istaskstarted 查询一个 Task 是否已经开始运行。
  • task_local_storage 操纵针对当前 Task 的键值存储。

Task 和事件

多数 Task 切换是在等待如 I/O 请求的事件,由 Julia Base 里的调度器执行。调度器维持一个可运行 Task 的队列,并执行一个事件循环,来根据例如收到消息等外部事件来重启 Task。

等待一个事件的基本函数是 wait。很多对象都实现了 wait 函数;例如,给定一个 Process 对象,wait 将等待它退出。wait 通常是隐式的,例如,wait 可能发生在调用 read 时等待数据可用。

在所有这些情况下,wait 最终会操作一个 Condition 对象,由它负责排队和重启 Task。当 Task 在一个 Condition 上调用 wait 时,该 Task 就被标记为不可执行,加到条件的队列中,并切回调度器。调度器将选择另一个 Task 来运行,或者阻止外部事件的等待。如果所有运行良好,最终一个事件处理器将在这个条件下调用 notify,使得等待该条件的 Task 又变成可运行。

调用 Task 显式创建的 Task 对于调度器时来说一开始时不知道的。如果你希望的话,你可以使用 yieldto 来人为管理 Task。但是当这种 Task 等待一个事件时,正如期待的那样,当事件发生时,它将自动重启。也能由调度器在任何可能的时候运行一个 Task,而无需等待任何事件。这可以调用 schedule,或者使用 @async

Task 的状态

Taskstate 属性来描述他们的执行状态。Task state 有:

符号/含义

:runnable 正在运行,或者可以被切换到

:waiting 被阻塞,等待一个特定事件

:queued 处在调度器中的运行队列中,即将被重启

:done 成功结束执行

:failed 以一个没被捕获的异常结束