931 ℉

17.02.08

Russ Cox的2017年Go开发计划

注: Russ Cox目前是Go Team的leader

我的目标是帮助开发者。我想确保我们Go团队所做的工作对Go开发人员有重大积极的影响。这听起来很容易,但是有很多问题,例如:花费太多时间清理或优化不需要的代码;只回应最常见或最近的投诉或要求;或过分侧重于短期改进。重要的是确保我们的开发工作集中在最有效的地方。

这篇文章概述了我今年的几个重点。这只是个人的看法,而不是Go团队的。

发布本文的一个原因是想收集反馈。如果你有任何想法或建议,请随时评论或新建GitHub issue。

另一个原因是让大家明白我们知道这些问题很重要。人们常常认为Go团队缺乏行动是因为我们认为一切都是完美的,而实际上是我们有其他更高优先级的工作要做。

Type aliases 类型别名

在大型代码库重构期间将类型从一个包移动到另一个包时存在重复的问题。 我们试图用通用别名来解决它,这至少引入两个问题:我们没有解释该变化带来的问题,我们没有及时交付,所以并没有在1.8发布该功能。 前事不忘,后事之师,我做了一个演讲并写了一篇关于type aliases基本问题的文章,并开始在Go issue tracker 上发起有关解决方案的讨论。 现在看起来更有限的type aliases是正确的选择。 我想确保type aliases可以顺利随Go 1.9发布。

Package management 包管理

我设计了 goinstall(它成为“go get”)工具。自那以后发生了很多变化。 特别是其他语言生态系统真正提升了人们对包管理的期望,开源世界大多数人同意语义化版本,这为推断版本兼容性提供了基础。 Go需要在这里做得更好,有一组代码贡献者一直在努力解决这一问题。 我想确保这些想法都很好地集成到标准的Go工具链中,并使包管理成为人们喜欢Go的理由。

Build Improvement

go构建系统的设计存在一些早该被修复的缺点。这里有三个代表性的例子,我打算重新设计go命令来解决这些问题。

build速度太慢,因为go命令不积极地缓存build结果。许多人没有意识到用go install可以保存build结果,而go build不是。因此他们运行重复的go build命令会减慢构建过程。当修改依赖的时候,同样的情况也发生在go test(应使用go test -i)。

测试结果也应该被缓存:如果测试的输入没有改变,那么通常不需要重新运行测试。这将使得在很少或没有变化时运行“all tests”的开销降低。

在GOPATH以外应该与在GOPATH内的工作一样。特别是可以git clone一个repo,运行go命令,并使它们工作正常。软件包管理只是使这一点变得更重要:您需要能够在不同版本的包(比如v1和v2)上工作,而不需要为它们分配完全独立的GOPATH。

代码语料库

从前面type aliases部分提到的文章和演讲可以看到很多语料库的实际使用。 我们还定义vet的插件必须针对真实程序中经常发生的问题。 我想看到对实际实践的分析成为我们讨论和评估Go变化的标准方式。

现在没有一个商定的代码语料库用于这些分析(每个人都必须首先创建自己的),对每个人来说这都是一个太繁重的工作。 我想开放一个单独的,自包含的Git repo,其中包括我们的用于分析的官方基准语料库。 一个可能的起点可能是GitHub上的前100个Go语言库。

Automatic vet

Go带有go vet这个强大的工具。 当vet有输出的时候,你应该检查vet输出。 每个人都必须运行它。 特别是,我认为我们可以在go测试期间运行vet,而不必减慢编译 - 编辑 - 测试周期。 如果我们能做到这一点,并且我们启用的vet检查限制为100%准确,我们可以让pass vet成为运行测试的前提条件。 开发人员不需要记得去运行go vet。 他们运行go test。

Errors & best practices

Go error的一部分是调用的函数以及相关的可用context,包括正在尝试的操作(例如函数名称及其参数)。 例如,这个程序:

err := os.Remove("/tmp/nonexist")
fmt.Println(err)

输出如下:

remove /tmp/nonexist: no such file or directory

然而并非所有的go代码都像os.Remove这样加入context,大部分代码都是这样:

if err != nil {    
    return err
}

沿着调用堆栈,抛弃有用的context(像remove /tmp/nonexist 等)。 我想尝试理解我们对包含context的期望是否错误,或者我们可以做一些事情来帮助编写返回更友好错误的代码。

在社区中还存在关于剥离错误context接口的各种讨论。 我想尝试理解什么时候有意义,以及是否应该通过该建议。

Context & best practices

我们在Go 1.7中添加了新的context包,用于保存请求相关的信息,如超时,取消状态和认证信息。一个独立的context是不可变的(像一个单独的字符串或int):它只能导出一个新的,更新后的context,并且将该context传递到调用栈或者(较不常见的)传回调用者。context现在诸如database/sql和net/http之类的API中使用,主要是为了当调用者不再对结果感兴趣时可以停止处理请求。超时信息适合在context中保存,但是数据库选项不是,因为不太可能在请求期间执行的所有可能的数据库操作。当前时钟源或日志接收器怎么办?是否适合存储在上下文中?我想尝试理解和描述适合使用context的场景。

内存模型

go内存模型相比其他语言需要更多的编译器参与工作。 我认为核心编译器和运行时开发人员都同意这些原子行为应该大致与C ++ seqcst atomics或Java volitiles相同,我们仍然需要仔细地在内存模型中写下来,也可能是写一篇长博客。

Immutability

race detector是Go最受欢迎的功能之一。 但没有race会更好。 如果有一些合理的方法来将不变性集成到Go,这样程序员可以做出明确的检查断言什么可以和不能写,从而消除编译时的某些竞争。 Go已经有一个不可变类型-string(string是不可变[]byte的类型)。 虽然我不认为今年会引入这一变化,但我想更好地了解该解决方案。 Javari,Midori,Pony和Rust都在解决方案讨论中提出了有趣的观点,除此之外还有大量的研究论文。

从长远来看,如果我们可以静态地消除竞争,那将消除对大部分内存模型的需要。 这可能是一个不可能实现的梦想,但我想更好地了解该解决方案。

泛型

没有什么比Go是否应该支持泛型(或者多少年前应该发生)这一问题 更加能引起Go和非Go开发者激烈的争论。 我不相信Go团队曾经说过“Go不需要泛型”。我们所说的是Go面临的更高优先级的问题。 例如,更好的软件包管理支持将对大多数Go开发人员产生比增加泛型更大的直接积极影响。 但我们确实明白,对于Go来说缺乏参数多态性是一个显着的障碍。

就个人而言,我想能够编写一般的channel处理函数,如:

// Join makes all messages received on the input channels
// available for receiving from the returned channel.
func Join(inputs ...<-chan T) <-chan T
// Dup duplicates messages received on c to both c1 and c2.
func Dup(c <-chan T) (c1, c2 <-chan T)

我还希望Go支持高级数据处理抽象(类似于FlumeJava或C#的LINQ),在编译时捕获类型错误,而不是在运行时。泛型还会带增加来通用数据结构或算法实现的可能,但我个人觉得广泛的应用程序更引人注目。

为了找到正确的方式为go添加泛型,我们已经努力了多年。一些人建议暂停设计通用参数多态性(如chan T)以及string和[]byte。如果后者通过不变性的参数化来处理(如在前面部分中所描述的)则可能简化对于泛型的设计的需求。

当我在2008年开始考虑Go的泛型时,学习的主要例子是C#,Java,Haskell和ML。没有一种方法是完美的。现在,还有更新的尝试,包括Dart,Midori,Rust和Swift。

这是几年来,我们探索的设计空间。考虑到对不可变性的理解和其他语言的实现,这可能是再次审视泛型的时候了。我不认为泛型将在今年引入,但我想说我可以更好地理解相关的解决方案。

jack.zh 标签:Go 继续阅读

856 ℉

16.10.09

程序员必须掌握的600个英语单词

application 应用程式 应用、应用程序
application framework 应用程式框架、应用框架 应用程序框架
architecture 架构、系统架构 体系结构
argument 引数(传给函式的值)。叁见 parameter 叁数、实质叁数、实叁、自变量
array 阵列 数组
arrow operator arrow(箭头)运算子 箭头操作符
assembly 装配件
assembly language 组合语言 汇编语言
assert(ion) 断言
assign 指派、指定、设值、赋值 赋值
assignment 指派、指定 赋值、分配
assignment operator 指派(赋值)运算子 = 赋值操作符
associated 相应的、相关的 相关的、关联、相应的
associative container 关联式容器(对应 sequential container) 关联式容器
atomic 不可分割的 原子的
attribute 属性 属性、特性
audio 音讯 音频
A.I. 人工智慧 人工智能
background 背景 背景(用於图形着色) 后台(用於行程)
backward compatible 回溯相容 向下兼容
bandwidth 频宽 带宽
base class 基础类别 基类
base type 基础型别 (等同於 base class)
batch 批次(意思是整批作业) 批处理
benefit 利益 收益
best viable function 最佳可行函式 最佳可行函式 (从 viable functions 中挑出的最佳吻合者)
binary search 二分搜寻法 二分查找
binary tree 二元树 二叉树
binary function 二元函式 双叁函数
binary operator 二元运算子 二元操作符
binding 系结 绑定
bit 位元 位
bit field 位元栏 位域
bitmap 位元图 位图
bitwise 以 bit 为单元逐一┅
bitwise copy 以 bit 为单元进行复制;位元逐一复制 位拷贝
block 区块,区段 块、区块、语句块
boolean 布林值(真假值,true 或 false) 布尔值
border 边框、框线 边框
brace(curly brace) 大括弧、大括号 花括弧、花括号
bracket(square brakcet) 中括弧、中括号 方括弧、方括号
breakpoint 中断点 断点
build 建造、构筑、建置(MS 用语)
build-in 内建 内置
bus 汇流排 总线
business 商务,业务 业务
buttons 按钮 按钮
byte 位元组(由 8 bits 组成) 字节
cache 快取 高速缓存
call 呼叫、叫用 调用
callback 回呼 回调
call operator call(函式呼叫)运算子调用操作符 ###### (同 function call operator)
candidate function 候选函式 候选函数 ###### (在函式多载决议程序中出现的候选函式)
chain 串链(例 chain of function calls) 链
character 字元 字符
check box 核取方块 (i.e. check button) 复选框
checked exception 可控式异常(Java)
check button 方钮 (i.e. check box) 复选按钮
child class 子类别(或称为derived class, subtype) 子类
class 类别 类
class body 类别本体 类体
class declaration 类别宣告、类别宣告式 类声明
class definition 类别定义、类别定义式 类定义
class derivation list 类别衍化列 类继承列表
class head 类别表头 类头
class hierarchy 类别继承体系, 类别阶层 类层次体系
class library 类别程式库、类别库 类库
class template 类别模板、类别范本 类模板
class template partial specializations ###### 类别模板偏特化 类模板部分特化
class template specializations ###### 类别模板特化 类模板特化
cleanup 清理、善后 清理、清除
client 客端、客户端、客户 客户
client-server 主从架构 客户/服务器
clipboard 剪贴簿 剪贴板
clone 复制 克隆
collection 群集 集合
combo box 复合方块、复合框 组合框
command line 命令列 命令行 (系统文字模式下的整行执行命令)
communication 通讯 通讯
compatible 相容 兼容
compile time 编译期 编译期、编译时
compiler 编译器 编译器
component 组件 组件
composition 复合、合成、组合 组合
computer 电脑、计算机 计算机、电脑
concept 概念 概念
concrete 具象的 实在的
concurrent 并行 并发
configuration 组态 配置
connection 连接,连线(网络,资料库) 连接
constraint 约束(条件)
construct 构件 构件
container 容器 容器 ###### (存放资料的某种结构如 list, vector…)
containment 内含 包容
context 背景关系、周遭环境、上下脉络 环境、上下文
control 控制元件、控件 控件
console 主控台 控制台
const 常数(constant 的缩写,C++ 关键字)
constant 常数(相对於 variable) 常量
constructor(ctor) 建构式 构造函数 ###### (与class 同名的一种 member functions)
copy (v) 复制、拷贝 拷贝
copy (n) 复件, 副本
cover 涵盖 覆盖
create 创建、建立、产生、生成 创建
creation 产生、生成 创建
cursor 游标 光标
custom 订制、自定 定制
data 资料 数据
database 资料库 数据库
database schema 数据库结构纲目
data member 资料成员、成员变数 数据成员、成员变量
data structure 资料结构 数据结构
datagram 资料元 数据报文
dead lock 死结 死锁
debug 除错 调试
debugger 除错器 调试器
declaration 宣告、宣告式 声明
deduction 推导(例:template argument deduction) 推导、推断
default 预设 缺省、默认
defer 延缓 推迟
define 定义 预定义
definition 定义、定义区、定义式 定义
delegate 委派、委托、委任 委托
delegation (同上)
demarshal 反编列 散集
dereference 提领(取出指标所指物体的内容) 解叁考
dereference operator dereference(提领)运算子 * 解叁考操作符
derived class 衍生类别 派生类
design by contract 契约式设计
design pattern 设计范式、设计样式 设计模式 ※ 最近我比较喜欢「设计范式」一词
destroy 摧毁、销毁
destructor 解构式 析构函数
device 装置、设备 设备
dialog 对话窗、对话盒 对话框
directive 指令(例:using directive) (编译)指示符
directory 目录 目录
disk 碟 盘
dispatch 分派 分派
distributed computing 分布式计算 (分布式电算) 分布式计算 分散式计算 (分散式电算)
document 文件 文档
dot operator dot(句点)运算子 . (圆)点操作符
driver 驱动程式 驱动(程序)
dynamic binding 动态系结 动态绑定
efficiency 效率 效率
efficient 高效 高效
end user 终端用户
entity 物体 实体、物体
encapsulation 封装 封装
enclosing class 外围类别(与巢状类别 nested class 有关)外围类
enum (enumeration) 列举(一种 C++ 资料型别) 枚举
enumerators 列举元(enum 型别中的成员) 枚举成员、枚举器
equal 相等 相等
equality 相等性 相等性
equality operator equality(等号)运算子 == 等号操作符
equivalence 等价性、等同性、对等性 等价性
equivalent 等价、等同、对等 等价
escape code 转义码 转义码
evaluate 评估、求值、核定 评估
event 事件 事件
event driven 事件驱动的 事件驱动的
exception 异常情况 异常
exception declaration 异常宣告(ref. C++ Primer 3/e, 11.3) 异常声明
exception handling 异常处理、异常处理机制 异常处理、异常处理机制
exception specification 异常规格(ref. C++ Primer 3/e, 11.4) 异常规范
exit 退离(指离开函式时的那一个执行点) 退出
explicit 明白的、明显的、显式 显式
export 汇出 引出、导出
expression 运算式、算式 表达式
facility 设施、设备 设施、设备
feature 特性
field 栏位,资料栏(Java) 字段, 值域(Java)
file 档案 文件
firmware 韧体 固件
flag 旗标 标记
flash memory 快闪记忆体 闪存
flexibility 弹性 灵活性
flush 清理、扫清 刷新
font 字型 字体
form 表单(programming 用语) 窗体
formal parameter 形式叁数 形式叁数
forward declaration 前置宣告 前置声明
forwarding 转呼叫,转发 转发
forwarding function 转呼叫函式,转发函式 转发函数
fractal 碎形 分形
framework 框架 框架
full specialization 全特化(ref. partial specialization)
function 函式、函数 函数
function call operator 同 call operator
function object 函式物件(ref. C++ Primer 3/e, 12.3) 函数对象
function overloaded resolution 函式多载决议程序 函数重载解决(方案)
functionality 功能、机能 功能
function template 函式模板、函式范本 函数模板
functor 仿函式 仿函式、函子
game 游戏 游戏
generate 生成
generic 泛型、一般化的 一般化的、通用的、泛化
generic algorithm 泛型演算法 通用算法
getter (相对於 setter) 取值函式
global 全域的(对应於 local) 全局的
global object 全域物件 全局对象
global scope resolution operator 全域生存空间(范围决议)运算子 :: 全局范围解析操作符
group 群组
group box 群组方块 分组框
guard clause 卫述句 (Refactoring, p250) 卫语句
GUI 图形介面 图形界面
hand shaking 握手协商
handle 识别码、识别号、号码牌、权柄 句柄
handler 处理常式 处理函数
hard-coded 编死的 硬编码的
hard-copy 硬拷图 屏幕截图
hard disk 硬碟 硬盘
hardware 硬体 硬件
hash table 杂凑表 哈希表、散列表
header file 表头档、标头档 头文件
heap 堆积 堆
hierarchy 阶层体系 层次结构(体系)
hook 挂钩 钩子
hyperlink 超链结 超链接
icon 图示、图标 图标
IDE 整合开发环境 集成开发环境
identifier 识别字、识别符号 标识符
if and only if 若且唯若 当且仅当
Illinois 伊利诺 伊利诺斯
image 影像 图象
immediate base 直接的(紧临的)上层 base class。 直接上层基类
immediate derived 直接的(紧临的)下层 derived class。 直接下层派生类
immutability 不变性
immutable 不可变(的)
implement 实作、实现 实现
implementation 实作品、实作体、实作码、实件 实现
implicit 隐喻的、暗自的、隐式 隐式
import 汇入 导入
increment operator 累加运算子 ++ 增加操作符
infinite loop 无穷回圈 无限循环
infinite recursive 无穷递回 无限递归
information 资讯 信息
infrastructure 公共基础建设
inheritance 继承、继承机制 继承、继承机制
inline 行内 内联
inline expansion 行内展开 内联展开
initialization 初始化(动作) 初始化
initialization list 初值列 初始值列表
initialize 初始化 初始化
inner class 内隐类别 内嵌类
instance 实体 实例 ###### (根据某种表述而实际产生的「东西」)
instantiated 具现化、实体化(常应用於 template) 实例化
instantiation 具现体、具现化实体(常应用於 template) 实例
integer (integral) 整数(的) 整型(的)
integrate 整合 集成
interacts 交谈、互动 交互
interface 介面 接口
for GUI 介面 界面
interpreter 直译器 解释器
invariants 恒常性,约束条件 约束条件
invoke 唤起 调用
iterate 迭代(回圈一个轮回一个轮回地进行) 迭代
exception 异常情况 异常
exception declaration 异常宣告(ref. C++ Primer 3/e, 11.3) 异常声明
exception handling 异常处理、异常处理机制 异常处理、异常处理机制
exception specification 异常规格(ref. C++ Primer 3/e, 11.4) 异常规范
exit 退离(指离开函式时的那一个执行点) 退出
explicit 明白的、明显的、显式 显式
export 汇出 引出、导出
expression 运算式、算式 表达式
facility 设施、设备 设施、设备
feature 特性
field 栏位,资料栏(Java) 字段, 值域(Java)
file 档案 文件
firmware 韧体 固件
flag 旗标 标记
flash memory 快闪记忆体 闪存
flexibility 弹性 灵活性
flush 清理、扫清 刷新
font 字型 字体
form 表单(programming 用语) 窗体
formal parameter 形式叁数 形式叁数
forward declaration 前置宣告 前置声明
forwarding 转呼叫,转发 转发
forwarding function 转呼叫函式,转发函式 转发函数
fractal 碎形 分形
framework 框架 框架
full specialization 全特化(ref. partial specialization)
function 函式、函数 函数
function call operator 同 call operator
function object 函式物件(ref. C++ Primer 3/e, 12.3) 函数对象
function overloaded resolution 函式多载决议程序 函数重载解决(方案)
functionality 功能、机能 功能
function template 函式模板、函式范本 函数模板
functor 仿函式 仿函式、函子
game 游戏 游戏
generate 生成
generic 泛型、一般化的 一般化的、通用的、泛化
generic algorithm 泛型演算法 通用算法
getter (相对於 setter) 取值函式
global 全域的(对应於 local) 全局的
global object 全域物件 全局对象
global scope resolution operator 全域生存空间(范围决议)运算子 :: 全局范围解析操作符
group 群组
group box 群组方块 分组框
guard clause 卫述句 (Refactoring, p250) 卫语句
GUI 图形介面 图形界面
hand shaking 握手协商
handle 识别码、识别号、号码牌、权柄 句柄
handler 处理常式 处理函数
hard-coded 编死的 硬编码的
hard-copy 硬拷图 屏幕截图
hard disk 硬碟 硬盘
hardware 硬体 硬件
hash table 杂凑表 哈希表、散列表
header file 表头档、标头档 头文件
heap 堆积 堆
hierarchy 阶层体系 层次结构(体系)
hook 挂钩 钩子
hyperlink 超链结 超链接
icon 图示、图标 图标
IDE 整合开发环境 集成开发环境
identifier 识别字、识别符号 标识符
if and only if 若且唯若 当且仅当
Illinois 伊利诺 伊利诺斯
image 影像 图象
immediate base 直接的(紧临的)上层 base class。 直接上层基类
immediate derived 直接的(紧临的)下层 derived class。 直接下层派生类
immutability 不变性
immutable 不可变(的)
implement 实作、实现 实现
implementation 实作品、实作体、实作码、实件 实现
implicit 隐喻的、暗自的、隐式 隐式
import 汇入 导入
increment operator 累加运算子 ++ 增加操作符
infinite loop 无穷回圈 无限循环
infinite recursive 无穷递回 无限递归
information 资讯 信息
infrastructure 公共基础建设
inheritance 继承、继承机制 继承、继承机制
inline 行内 内联
inline expansion 行内展开 内联展开
initialization 初始化(动作) 初始化
initialization list 初值列 初始值列表
initialize 初始化 初始化
inner class 内隐类别 内嵌类
instance 实体 实例 ###### (根据某种表述而实际产生的「东西」)
instantiated 具现化、实体化(常应用於 template) 实例化
instantiation 具现体、具现化实体(常应用於 template) 实例
integer (integral) 整数(的) 整型(的)
integrate 整合 集成
interacts 交谈、互动 交互
interface 介面 接口
for GUI 介面 界面
interpreter 直译器 解释器
invariants 恒常性,约束条件 约束条件
invoke 唤起 调用
iterate 迭代(回圈一个轮回一个轮回地进行) 迭代
iterative 反覆的,迭代的
iterator 迭代器(一种泛型指标) 迭代器
iteration 迭代(回圈每次轮回称为一个 iteration) 迭代
item 项目、条款 项、条款、项目
laser 雷射 激光
level 阶 层 (级) 例 high level 高阶 高层
library 程式库、函式库 库、函数库
lifetime 生命期、寿命 生命期、寿命
link 联结、连结 连接,链接
linker 联结器、连结器 连接器
literal constant 字面常数(例 3.14 或 “hi” 这等常数值) 字面常数
list 串列(linked-list) 列表、表、链表
list box 列表方块、列表框 列表框
load 载入 装载
loader 载入器 装载器、载入器
local 区域的(对应於 global) 局部的
local object 区域物件 局部对象
lock 机锁
loop 回圈 循环
lvalue 左值 左值
macro 巨集 宏
magic number 魔术数字 魔法数
maintain 维护 维护
manipulator 操纵器(iostream 预先定义的一种东西) 操纵器
marshal 编列 列集 叁考 demarshal
mechanism 机制 机制
member 成员 成员
member access operator 成员取用运算子(有 dot 和 arrow 两种) 成员存取操作符
member function 成员函式 成员函数
member initialization list 成员初值列 成员初始值列表
memberwise 以 member 为单元┅、members 逐一┅ 以成员为单位
memberwise copy 以 members 为单元逐一复制
memory 记忆体 内存
menu 表单、选单 菜单
message 讯息 消息
message based 以讯息为基础的 基於消息的
message loop 讯息回圈 消息环
method (java) 方法、行为、函式 方法
meta- 超- 元- 例 meta-programming 超编程 元编程
micro 微 微
middleware 中介层 中间件
modeling 模塑
modeling language 塑模语言,建模语言
modem 数据机 调制解调器
module 模组 模块
modifier 饰词 修饰符
most derived class 最末层衍生类别 最底层的派生类
mouse 滑鼠 鼠标
mutable 可变的 可变的
multi-tasking 多工 多任务
namespace 命名空间 名字空间、命名空间
native 原生的 本地的、固有的
nested class 巢状类别 嵌套类
network 网路 网络
network card 网路卡 网卡
object 物件 对象
object based 以物件为基础的 基於对象的
object file 目的档 目标文件
object model 物件模型 对象模型
object oriented 物件导向的 面向对象的
online 线上 在线
opaque 不透明的
operand 运算元 操作数
operating system (OS) 作业系统 操作系统
operation 操作、操作行为 操作
operator 运算子 操作符、运算符
option 选项,可选方案 选项
ordinary 常规的 常规的
overflow 上限溢位(相对於 underflow) 溢出(underflow:下溢)
overhead 额外负担、额外开销 额外开销
overload 多载化、多载化、重载 重载
overloaded function 多载化函式 重载的函数
overloaded operator 多载化运算子 被重载的操作符
overloaded set 多载集合 重载集合
override 改写、覆写 重载、改写、重新定义 ###### (在 derived class 中重新定义虚拟函式
package 套件 包
pair 对组
palette 调色盘、组件盘、工具箱
pane 窗格 窗格 ###### (有时为嵌板之意,例 Java Content Pane)
parallel 平行 并行
parameter 叁数(函式叁数列上的变数) 叁数、形式叁数、形叁
parameter list 叁数列 叁数列表
parent class 父类别(或称 base class) 父类
parentheses 小括弧、小括号 圆括弧、圆括号
parse 解析 解析
part 零件 部件
partial specialization 偏特化(ref. C++ Primer 3/e, 16.10) 局部特化 ###### (ref. full specialization)
pass by address 传址(函式引数的传递方式)(非正式用语)传地址
pass by reference 传址(函式引数的一种传递方式) 传地址, 按引用传递
pass by value 传值(函式引数的一种传递方式) 按值传递
pattern 范式、样式 模式
performance 效率、性能兼而有之 性能
persistence 永续性 持久性
pixel 图素、像素 像素
placement delete ref. C++ Primer 3/e, 15.8.2
placement new ref. C++ Primer 3/e, 15.8.2
platform 平台 平台
pointer 指标 指针 址位器(和址叁器 reference 形成对映,满好)
poll 轮询 轮询
polymorphism 多型 多态
pop up 冒起式、弹出式 弹出式
port 埠 端口
postfix 后置式、后序式 后置式
precedence 优先序(通常用於运算子的优先执行次序)
prefix 前置式、前序式 前置式
preprocessor 前处理器 预处理器
prime 质数 素数
primitive type 基本型别 (不同於 base class,基础类别)
print 列印 打印
printer 印表机 打印机
priority 优先权 (通常用於执行绪获得 CPU 时间的优先次序)
procedure 程序 过程
procedural 程序性的、程序式的 过程式的、过程化的
process 行程 进程
profile 评测 评测
profiler 效能(效率)评测器 效能(性能)评测器
programmer 程式员 程序员
programming 编程、程式设计、程式化 编程
progress bar 进度指示器 进度指示器
project 专案 项目、工程
property 属性
protocol 协定 协议
pseudo code 假码、虚拟码、伪码 伪码
qualified 经过资格修饰(例如加上 scope 运算子) 限定
qualifier 资格修饰词、饰词 限定修饰词
quality 品质 质量
queue 伫列 队列
radian 径度 弧度
radio button 圆钮 单选按钮
raise 引发(常用来表示发出一个 exception) 引起、引发
random number 随机数、乱数 随机数
range 范围、区间(用於 STL 时) 范围、区间
rank 等级、分等(ref. C++Primer 3/e 9,15章) 等级
raw 生鲜的、未经处理的 未经处理的
record 记录 记录
recordset 记录集 记录集
recursive 递回 递归
re-direction 重导向 重定向
refactoring 重构、重整 重构
refer 取用 叁考
refer to 指向、指涉、指代
reference (C++ 中类似指标的东西,相当於 “化身”) 引用、叁考 址叁器, see pointer
register 暂存器 寄存器
reflection 反射 反射、映像
relational database 关联式资料库 关系数据库
represent 表述,表现 表述,表现
resolve 决议(为算式中的符号名称寻找 解析 对应之宣告式的过程)
resolution 决议程序、决议过程 解析过程
resolution 解析度 分辨率
restriction 局限
return 传回、回返 返回
return type 回返型别 返回类型
return value 回返值 返回值
robust 强固、稳健 健壮
robustness 强固性、稳健性 健壮性
routine 常式 例程
runtime 执行期 运行期、运行时
common language runtime (CLR) 译为「通用语言执行层」
rvalue 右值 右值
save 储存 存储
schedule 排程 调度
scheduler 排程器 调度程序
scheme 结构纲目、组织纲目
scroll bar 卷轴 滚动条
scope 生存空间、生存范围、范畴、作用域 生存空间
scope operator 生存空间(范围决议)运算子 :: 生存空间操作符
scope resolution operator 生存空间决议运算子 生存空间解析操作符 ###### (与scope operator同)
screen 萤幕 屏幕
search 搜寻 查找
semantics 语意 语义
sequential container 序列式容器 顺序式容器 ###### (对应於 associative container)
server 伺服器、伺服端 服务器、服务端
serial 串行
serialization 次第读写,序列化 序列化 (serialize)
setter (相对於 getter) 设值函式
signal 信号
signature 标记式、签名式、署名式 签名
slider 滚轴 滑块
slot 条孔、槽 槽
smart pointer 灵巧指标、精灵指标 智能指针
snapshot 萤幕快照(图) 屏幕截图
specialization 特殊化、特殊化定义、特殊化宣告 特化
specification 规格 规格、规范
splitter 分裂视窗 切分窗口
software 软体 软件
solution 解法,解决方案 方案
source 原始码 源码、源代码
stack 堆叠 栈
stack unwinding 堆叠辗转开解(此词用於 exception 主题) 栈辗转开解 *
standard library 标准程式库
standard template library 标准模板程式库
statement 述句 语句、声明
status bar 状态列、状态栏 状态条
STL 见 standard template library
stream 资料流、串流 流
string 字串 字符串
subroutine
subscript operator 下标运算子 [ ] 下标操作符
subtype 子型别 子类型
support 支援 支持
suspend 虚悬 挂起
symbol 符号 记号
syntax 语法 语法
tag 标签 标记 索引标签,页签
target 标的(例 target pointer:标的指标) 目标
task switch 工作切换 任务切换
template 模板、范本 模板
template argument deduction 模板引数推导 模板叁数推导
template explicit specialization 模板显式特化(版本) 模板显式特化
template parameter 模板叁数 模板叁数
temporary object 暂时物件 临时对象
text 文字 文本

jack.zh 标签:英语 继续阅读

1102 ℉

16.04.14

科比退役了我的青春

    我的一个梦想就是有一天去斯台普斯球馆,看一场科比的湖人比赛,无论对手是谁不重要,而今天,他小子居然退役了,这真是一个遗憾。

    科比退役了,今天最后一场球,可今天的这篇文字,可能煽情的依然是我的青春而不是唠嗑,这多少有点煞风景,可在这个日子里,无数人用这个身影来缅怀自己逐渐老去的一代人的青春,也就算是对唠嗑的一种致敬吧。

    我是一个狂热的篮球迷,从青葱,到成年,一直都是。

    我最喜欢的篮球员动员是科比,肯定的啊,那么杰出,纯粹,优美的篮球员动员,谁会不喜欢呢!

    科比退役了,今天是他最后的一场球赛,说实话,我在起初,没感觉到什么,照常上着班,偶尔拿出手机,来回切换着用文字直播看着湖人还有勇士的比赛情况,开始也只是想着,“哦,真应该请个假去看直播”,直到,通过文字直播,科比的执拗的热情刺穿我本已麻木的热情。

    张公子说:“真正的科比也许早离开了,今天只是为了告别而归来。”,作为一个老湖人球迷,最近三年,我已经不怎么关注湖人的比赛,确实是那样,科比,在我心中,已经退役很久了,远在那次跟腱的断裂,他的时代就已经结束了,偶尔在某些科黑无脑的谩骂评论中,愤愤然的想着怎么能对一个如此坚强的斗士喷这些言语。直到:

第四节中段 追身三分得手,第40分。全场开始喊科比。大概是这歌声,振奋了他。

从这时候开始,进入科比节奏。

所谓科比节奏,即是说,进入NBA历史上几乎绝无仅有的,“我知道这不是投篮好机会,但我就是手起刀落投得中”,tough shot maker。

弧顶急停三分,39投16中43分。

右翼急停出手敲篮筐后沿后,再补上两次投篮。得到第45分

之后,进入黑曼巴状态。

后面的疯狂,真正的球迷,都会被激动的热泪盈眶

    真的,你无法描述那种震撼,就如你的青春一下子回来了,就如,你又找到了那群一起打球一起打架的家伙,找到了那个给你初恋悸动又苦涩的姑娘,回到了那个怀揣理想未来无限的时代…,虽然,我这个时候也只能在还算隐蔽的工位上假装有一粒来自遥远的沙子钻进了了我的眼睛。

青春不死,只是退役,开启另一段旅程!

    98年,我上初中,开始的疯狂的中学生活,那时候我瘦小,阴郁,傲慢,暴力。也许是出色的学习成绩给了我最大的自信,那个时候感觉自己无所不能,感觉自己将来是个上天入地的人物。我也是从那个时候,知道了了NBA和科比,虽然那还是乔丹的时代。可我那个时候身材矮小,再加上本身运动细胞就不是很发达,所以根本和篮球不沾边,主要的运动就是跑步,什么都不想,看着前方十米的地面,就这样一直跑下去,跟随头上4/6分头长发的飘动节奏。直到初四的时候疯狂长个子到了170,才能把篮球扔到篮筐的高度,那是2002年,OK三连冠,如日中天,科比的海报也偶尔在那个贫穷的乡下中学附近的小卖部买到,我也只是知道,那是一个长得秀气的,眼神犀利的黑人小子。

    03年上高中,去了县城,可以看得见CCTV5的体育频道了,各种海报更是满天飞,也许是先入为主的原因,对科比比较有好感,那个时候身体虚弱,经常生病,去学校的周边打点滴,看的大部分球赛,居然是在病床上挂着点滴看着输液室的电视,也是那几年,才真正的看了一些NBA的比赛,真正建立起来对科比的喜爱。

    高中,我依然是老师的宠儿,基本垄断大部分考试班级的第一,还经常地请假,球场飞奔,擅长长跑,基本从不做厚厚的习题集。那个时候感觉自己的存在就是一种碾压,甚至会感到对那些好好学习还被碾压的同学的一种不尊重,想想自己如果努力一点,还不是会上天啊。那时候喜欢科比,也是因为喜欢科比的那种碾压一切的气质,那种清楚地隔着屏幕都能让所有的对手感受到,“今年我们搞不定他”。就是这种感觉。

    所以我看着科比的球赛,碾压着我的同学,也碾压者我的青春,去夺得校运会的5000米冠军,去拿奥赛物理的全国二等奖,去拿校办数理化的第一名,甚至去拿是个朗诵大赛的全校的第一名,用自己写的诗歌,在一张别人都认不清的草稿纸上半成品….,班主任老魏跟对我期望深重的老爸说:”即使考不好,也能走南开那个级别的吧“

    直到自负和病魔摧毁我….

    我抱着欲裂的脑袋,疼的睡不着觉,疼的流泪,然而继续逃课,打球,看球,输液,奔溃… 无休无止,恶性循环,那是科比最孤单的也是最执拗的05,06年…

    我进了一所二流大学,离开了家乡,离开了最让我风光也让我终点落魄的中学时代

    在大学,我很少落下科比的湖人比赛,跟周围的哥们一起打球,跟詹密,真理密,加内特蜜,绿衫军密争论,然后继续着我的沉沦,基本不去上课,大学四年班级的同学认不全…,看着科比迎来他的家嫂和第二段辉煌,我却挥霍着自己的日子,深陷一系列的失落无法自拔,还在经常地梦见回去复读,但是却一道题都不会做了,一身冷汗的惊醒…

    时光总是匆匆,10年毕业,参加工作,然后紧接着家庭的变故,父亲的离世,我的噩梦变成了两个。可生活还在继续,我们也必须承担起自己造就的历史,去承担一个男人该承担的责任。

    也许,当有了一分责任,你才会真正的成长为一个男人。

    就如科比抗争的这三年,再难,也要当一个硬汉

    其实许多人都是如此。少年时,追着每场球看,看到了最巅峰的时刻,痴狂地爱上;之后,便是时光流逝,物是人非。有人要换学校,有人要毕业工作,有人有了家庭。这个时代资讯发达,许多人已经来不及看完一场球。偶尔和原来的球迷同学一起聊一下,大概的感觉也是如此:

“哦?科比还在打球。哦?科比又得了多少分啦?”

    不只是对科比。许多人都是如此。年纪长了,时间少了,许多爱,分不过来了。

    今天,那个执拗的男人正式的完成了最后的一场NBA表演,不再是怒目抿嘴,叼着球衣,而是一种严肃,疲劳却依旧坚定,他开始了另一段旅程,我也不再青葱,每日兢兢业业的上班,维护者一家的老小。

    喜欢他,也许是喜欢他的执拗坚韧纯粹,把他定位自己的模版,而回顾自己的过往,也许更像伤仲永,说得好一点,也就是麦迪,挥霍了自己的天赋和青春,可即使是这样,看见那个坚韧的男人,还是会想:他退役的只是我的青春,而不是我的人生,那个不甘一生平凡的我的人生。

    附科比素描

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

KB

jack.zh 标签:NBA 继续阅读

1043 ℉

16.03.17

2016计划

关于2016年的年度计划,我想了很久,不知道怎么面对2016的变化和迷茫,始终也就没有书写,可生活在继续,有些事情,也得积极的有计划的面对。

这里就不说2016年初的迷茫了,毕竟,明天是新的一天,向前看吧。

2016的计划比较详细和琐碎,压力也比较大,后面自适应吧

关于工作

2016换了新的工作,进入了一家做产品,做平台的公司,这个是我一直想要的,但也多了些许叶公好龙的意味,每个公司有每个公司的问题,每个公司都会有一些个人的不爽,担心的环境,还是要适应,我需要做的是:

  • 1:保质保量的完成任务
  • 2:保持虚心和耐心
  • 3:保持前瞻和OwnerShip
  • 4:完成充分的利用每周的40个小时
  • 5:保持学习探索的激情

关于技术栈

技术栈的主要方向是英语,写作,积累

  • 1:完成Lua的书籍50% 即Lua语言基础和Lua语言进阶(C API)的书写
  • 2:完成自己的源码阅读和TCP IP的实现
  • 3:完成每天20个单词
  • 4:完成一个20W字书记翻译(具体翻译书籍待定)

关于生活

因为后半年会成为两个孩子的爸爸,这是一份荣耀喜悦,也是一份责任,随着自己的迈过30的门槛,妈妈老去,孩子成长,也许,这才是生活

2016年,家庭和睦健康,孩子快乐成长,自己保持心态和身体健康,鉴于自己身体状态一般,把这个作为2016的头等大事,因为只有自己的健康,才会照顾好家人

  • 1:坚持锻炼身体,保持骑行等必要的出行方式
  • 2:保持体重,2016年的目标是把体重降到160以下
  • 3:保持耐心和激情
  • 4:热爱生活

修炼清单

书籍篇:

前三本网络基础书是为了夯实基本功,为了将来的基础和Lua书籍的写作

深入理解计算机系统时为了夯实自己的基础,毕竟大学没学习啊

Python主要三个方向 Django Python基础和源码分析

学习Nginx和Lua

夯实操作系统原理基础

学习机器学习基础

写书篇:

  • 《Lua语言基础》(必须)【:6.30】
  • 《Lua语言进阶》(必须)【:7.30】
  • 《Lua实战:从0开始做一个完备的Lua Web框架》(必须)【:12.31】
  • 《Lua源码剖析》(选做)
  • 《Tornado源码剖析》(选做)

项目:

  • 《ngrok的汉化注释fork》(必须)【:8.31】
  • 《pyngrok》(选做)
  • 《zlua》(必须)【:10.31】

翻译:

  • 三选一或多
  • 《OReilly.Introducing.Go.2016.1》(必须)【:6.30】
  • 《Programming in Lua third edition》(选做)【:6.30】
  • 《Django Design Patterns and Best Practices》(选做)【:12.31】

文青:

《人间的表层:绝望》
  • 进化
  • 以人为中心的塌陷
  • 人类的基金与反击
  • 费米悖论
  • 终结
《人间的表层:罪恶》
  • 大象的退却
  • 十亿城民
  • 强国与强民
  • 哲学与宗教
  • 不是历史
《人间的表层:渺小》
  • 宇宙的形成
  • 上帝粒子
  • 希格斯粒子
  • 时间与空间
  • 在时间上和空间上不存在的人类
  • 电影:2001星际漫游指南
《老无所依》
  • 诗人的儿子
  • 众生相
  • 犹疑
  • 消亡

jack.zh 继续阅读

1311 ℉

16.02.18

Python发送邮件处理

今天要说的是Python发送邮件,其实发送邮件的代码都差不多,但是使用其他的邮件服务器,还是有很多的限制,这里就简要说明一下使用其他的邮件服务器发送邮件和自己建立邮件服务器发送邮件,主要是贴代码。

系统 Ubuntu 14.04.3

用163服务器发送邮件

优点:少量邮件需要发送的时候还是可以的,不用自建服务器,减少了服务器的开销

缺点:频繁发送会有问题,网络延迟严重,异步IO回调慢等

不要吐槽我用start_new_thread

# -*- coding: UTF-8 -*-
'''
jack.zh sen mail test.
'''
import thread        
import smtplib
from email.mime.text import MIMEText

from log import logger

From_email = {
    "mail_host" :"smtp.163.com",  #设置服务器
    "mail_user" :"user_name",    #用户名
    "mail_pass" :"xxxxxxxx",   #口令 
    "mail_postfix" :"163.com"  #发件箱的后缀 
}

def send_mail(to_list, sub, content):
    me="my_name"+"<"+From_email["mail_user"]+"@"+From_email["mail_postfix"]+">"
    msg = MIMEText(content,_subtype='html',_charset='utf-8')
    msg['Subject'] = sub
    msg['From'] = me
    msg['To'] = ";".join(to_list)
    try:
        server = smtplib.SMTP()
        server.connect(From_email["mail_host"])
        server.login(From_email["mail_user"],From_email["mail_pass"])
        server.sendmail(me, to_list, msg.as_string())
        server.close()
        return True
    except Exception, e:
        logger.error(str(e))
        logger.error(str(to_list))
        logger.error(str(sub))
        logger.error(str(content))


def send_mail_thread(mailto_list, subject, msg):
    if send_mail(mailto_list, subject, msg):
        logger.info("send mail success.") 
    else:
        logger.error("send mail fail.")
        logger.error(subject)
        logger.error(msg)


def util_send_email(msg, subject, emails):
    thread.start_new_thread(send_mail_thread, (emails, subject, msg))


if __name__ == '__main__':
    msg = "msg"

    mailto_list=["zzh.coder@qq.com"]
    if send_mail(mailto_list,"hello", msg):
        logger.error("yes")
    else:
        logger.error("no")

使用自建邮箱服务器发送sendmail

安装邮件服务:

apt-get install sendmail sendmail-cf squirrelmail spamassassin mailman mailutils sharutils

贴发送邮件代码:

#!/usr/bin/env python
#@author : zzh.coder@qq.com
#@desc: for mail sending.

import smtplib
import getopt
import sys
import os

from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase

from email.MIMEText import MIMEText
import email.Encoders as encoders


def send_mail(mail_from, mail_to, subject, msg_txt, files=[]):
    # Create message container - the correct MIME type is multipart/alternative.
    msg = MIMEMultipart('alternative')
    msg['Subject'] = subject
    msg['From'] = mail_from
    msg['To'] = mail_to

    # Create the body of the message (a plain-text and an HTML version).
    #text = msg
    html = msg_txt

    # Record the MIME types of both parts - text/plain and text/html.
    #part1 = MIMEText(text, 'plain')
    part2 = MIMEText(html, 'html')

    # Attach parts into message container.
    # According to RFC 2046, the last part of a multipart message, in this case
    # the HTML message, is best and preferred.
    #msg.attach(part1)
    msg.attach(part2)

    #attachment
    for f in files:
        #octet-stream:binary data
        part = MIMEBase('application', 'octet-stream')
        part.set_payload(open(f, 'rb').read())
        encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(f))
        msg.attach(part)

    # Send the message via local SMTP server.
    s = smtplib.SMTP('localhost')
    # sendmail function takes 3 arguments: sender's address, recipient's address
    # and message to send - here it is sent as one string.

    mailto_list = mail_to.strip().split(",")
    if len(mailto_list) > 1:
        for mailtoi in mailto_list:
            s.sendmail(mail_from, mailtoi.strip(), msg.as_string())
    else:
        s.sendmail(mail_from, mail_to, msg.as_string())

    s.quit()
    return True


def main():
    files = []
    try:
        opts, args = getopt.getopt(sys.argv[1:], "f:t:s:m:a:")
        for op, value in opts:
            if op == "-f":
                mail_from = value
            elif op == "-t":
                mail_to = value
            elif op == "-s":
                subject = value
            elif op == "-m":
                msg_txt = value
            elif op == "-a":
                files = value.split(",")
    except getopt.GetoptError:
        print(sys.argv[0] + " : params are not defined well!")

    print mail_from, mail_to, subject, msg_txt
    if files:
        send_mail(mail_from, mail_to, subject, msg_txt, files)
    else:
        send_mail(mail_from, mail_to, subject, msg_txt)

if __name__ == "__main__":
    main()

Demo over。

后面的说明:

  • 服务器需要接收邮件并处理等操作此处不说,那是一个比较庞大的问题了
  • 自建服务器也有失败的可能,在配置senmail的时候有很多配置需要了解,这里不做详解,只提供思路
  • 方法2完全可以指定发件箱的地址,在此发件箱去接收那边的回复邮件,手动处理
  • 方法一和二都可以完整的使用邮件基本的所有功能,比如模版,附件等等

jack.zh 标签:Python 继续阅读

1263 ℉

16.01.22

Tomorrow is another day

偶然看见一个好玩的异步代码提供了神奇的装饰器的Python第三方库Tomorrow,查了下他的代码,短小精悍,却包含了很多有意思的特性,这篇文章就是从这个库说开去的。

来个使用的例子:

不使用tomorrow的
import time
import requests

urls = [
    'http://sina.com.cn',
    'http://163.com',
    'http://oschina.net',
    'http://baidu.com',
    'http://csdn.net',
]

def download(url):
    return requests.get(url)

if __name__ == "__main__":

    start = time.time()
    responses = [download(url) for url in urls]
    html = [response.text for response in responses]
    end = time.time()
    print ("Time: %f seconds" % (end - start))

# Time: 2.059337 seconds
tomorrow一下
import time
import requests

from tomorrow import threads

urls = [
    'http://sina.com.cn',
    'http://163.com',
    'http://oschina.net',
    'http://baidu.com',
    'http://csdn.net',
]

@threads(5)
def download(url):
    return requests.get(url)

if __name__ == "__main__":
    start = time.time()
    responses = [download(url) for url in urls]
    html = [response.text for response in responses]
    end = time.time()
    print ("Time: %f seconds" % (end - start))
# Time: 0.303633 seconds

** 2.059337 seconds VS 0.303633 seconds ** 你看的没错,就是这么神奇

为了探明究竟,自古华山一条路,查代码,OMG,代码只有42行,全贴:

from functools import wraps

from concurrent.futures import ThreadPoolExecutor


class Tomorrow():

    def __init__(self, future, timeout):
        self._future = future
        self._timeout = timeout

    def __getattr__(self, name):
        result = self._wait()
        return result.__getattribute__(name)

    def _wait(self):
        return self._future.result(self._timeout)


def async(n, base_type, timeout=None):
    def decorator(f):
        if isinstance(n, int):
            pool = base_type(n)
        elif isinstance(n, base_type):
            pool = n
        else:
            raise TypeError(
                "Invalid type: %s"
                % type(base_type)
            )
        @wraps(f)
        def wrapped(*args, **kwargs):
            return Tomorrow(
                pool.submit(f, *args, **kwargs),
                timeout=timeout
            )
        return wrapped
    return decorator


def threads(n, timeout=None):
    return async(n, ThreadPoolExecutor, timeout)

这里面主要包含了几个方面

  • 第三方库Requests
  • Python装饰器
  • Python的几个基本的Metaclasses() __getattr__ __getattribute__
  • 异步杀器 concurrent.futures

Requests 是使用 Apache2 Licensed 许可证的 HTTP 库。用 Python 编写,真正的为人类着想。在大部分第三方库的推荐中名列前茅。这里不做过多的说明。

Python装饰器是Python编程中的重要组成部分,也是函数式编程的重要概念,我相信我不会比coolshellPython修饰器的函数式编程写的更好,所以想了解的去看这篇文章吧。

Python的Metaclasses是Python编程的重要概念,推荐你去看Python Pocket Reference的Operator Overloading Methods章节,另外说一下,作为手册,就应该像这本书这样写。

我们今天主要说的是concurrent.futures

在Python2里面,concurrent.futures是一个第三方库,在Python3里面已经作为标准库提供了,Python3相关的文档看这里

准备

在Python2环境里,需要安装的依赖库

pip2 install futures
pip2 install requests

Python3环境

pip3 install requests

对于python来说,作为解释型语言,Python的解释器必须做到既安全又高效。我们都知道多线程编程会遇到的问题,解释器要留意的是避免在不同的线程操作内部共享的数据,同时它还要保证在管理用户线程时保证总是有最大化的计算资源。而python是通过使用全局解释器锁来保护数据的安全性: python代码的执行由python虚拟机来控制,即Python先把代码(.py文件)编译成字节码(字节码在Python虚拟机程序里对应的是PyCodeObject对象,.pyc文件是字节码在磁盘上的表现形式),交给字节码虚拟机,然后虚拟机一条一条执行字节码指令,从而完成程序的执行。python在设计的时候在虚拟机中,同时只能有一个线程执行。同样地,虽然python解释器中可以运行多个线程,但在任意时刻,只有一个线程在解释器中运行。而对python虚拟机的访问由全局解释器锁来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程的环境中,python虚拟机按一下方式执行:

  • 1,设置GIL(global interpreter lock).
  • 2,切换到一个线程执行。
  • 3,运行:
    • a,指定数量的字节码指令。
    • b,线程主动让出控制(可以调用time.sleep(0))。
  • 4,把线程设置为睡眠状态。
  • 5,解锁GIL.
  • 6,再次重复以上步骤。

GIL的特性,也就导致了python不能充分利用多核cpu。而对面向I/O的(会调用内建操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果线程并为使用很多I/O操作,它会在自己的时间片一直占用处理器和GIL。这也就是所说的:I/O密集型python程序比计算密集型的程序更能充分利用多线程的好处。

总之,不要使用python多线程,使用python多进程进行并发编程,就不会有GIL这种问题存在,并且也能充分利用多核cpu。

GIL的特性,也就导致了python不能充分利用多核cpu。而对面向I/O的(会调用内建操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果线程并为使用很多I/O操作,它会在自己的时间片一直占用处理器和GIL。这也就是所说的:I/O密集型python程序比计算密集型的程序更能充分利用多线程的好处。 总之,不要使用python多线程,使用python多进程进行并发编程,就不会有GIL这种问题存在,并且也能充分利用多核cpu。

concurrent.futures提供的功能:

提供了多线程(ThreadPoolExecutor)和多进程(ProcessPoolExecutor)的并发功能

concurrent.futures基本方法:

class   concurrent.futures.Executor
#Executor为ThreadPoolExecutor或者ProcessPoolExecutor
concurrent.futures提供的方法如下:

1. submit(fn, *args, **kwargs)

fn:为需要异步执行的函数

args,kwargs:为给函数传递的参数

例:

#!/bin/env python
#coding:utf-8
import time,re
import os,datetime
from concurrent import futures

def wait_on_b():
    print 5
    time.sleep(2)

def wait_on_a():
    print 6
    time.sleep(2)

ex = futures.ThreadPoolExecutor(max_workers=2)
ex.submit(wait_on_b)
ex.submit(wait_on_a)
#wait_on_a和wait_on_b函数会同时执行,因为使用了2个worker

2. map(func, *iterables, timeout=None)

此map函数和python自带的map函数功能类似,只不过concurrent模块的map函数从迭代器获得参数后异步执行。并且,每一个异步操作,能用timeout参数来设置超时时间,timeout的值可以是int或float型,如果操作timeout的话,会raisesTimeoutError。如果timeout参数不指定的话,则不设置超时间。

func:为需要异步执行的函数

iterables:可以是一个能迭代的对象,例如列表等。每一次func执行,会从iterables中取参数。

timeout:设置每次异步操作的超时时间

例:

#!/bin/env python
#coding:utf-8
import time,re
import os,datetime
from concurrent import futures
data = ['1','2']

def wait_on(argument):
    print argument
    time.sleep(2)
    return 'ok'

ex = futures.ThreadPoolExecutor(max_workers=2)
for i in ex.map(wait_on,data):
    print i

map函数异步执行完成之后,结果也是list,数据需要从list中取出

submit函数和map函数,根据需要,选一个使用即可。

3. shutdown(wait=True)

此函数用于释放异步执行操作后的系统资源。

一个完整的concurrent例子

#!/bin/env python
#coding:utf-8

import time,re,fcntl
import os,datetime

from concurrent import futures

count_list = list()
minute_num = 1
start_time = datetime.datetime(2016, 1, 22, 13, 30, 0, 484870)
now = datetime.datetime.now()
os.system(':>new.txt')
f_new = open('new.txt','a')

def test(count_time_format):
    f = open('push_slave.stdout','r')
    for line in f.readlines():
        if re.search(count_time_format,line):
            #获得文件专用锁
            fcntl.flock(f_new, fcntl.LOCK_EX)
            f_new.writelines(line)
            f_new.flush()

            #释放文件锁
            fcntl.flock(f_new, fcntl.LOCK_UN)
            break

while 1:
    after_one_minute = datetime.timedelta(minutes=minute_num)
    count_time = after_one_minute + start_time
    count_time_format = count_time.strftime('%Y-%m-%d %H:%M')
    minute_num = minute_num+1
    count_list.append(count_time_format)
    if count_time_format == "2014-04-23 16:00":
        break

def exec_cmd():
    with futures.ProcessPoolExecutor(max_workers=24) as executor:
        dict((executor.submit(test, times), times) for times in count_list)

if __name__ == '__main__':
    exec_cmd()
    f_new.close()

收工,有时间比较一下 futuresmultiprocessing

jack.zh 标签:Python 继续阅读

1181 ℉

16.01.06

聊聊Docker 1.9的新网络特性

转自林帆 AlaudaCloud

Docker在1.9版本中引入了一整套的自定义网络命令和跨主机网络支持。这是libnetwork项目从Docker的主仓库抽离之后的一次重大变化。不论你是否已经注意到了,Docker的网络新特性即将对用户的习惯产生十分明显的改变。

1. libnetwork和Docker网络

ibnetwork项目从lincontainer和Docker代码的分离早在Docker 1.7版本就已经完成了(从Docker 1.6版本的网络代码中抽离)。在此之后,容器的网络接口就成为了一个个可替换的插件模块。由于这次变化进行的十分平顺,作为Docker的使用者几乎不会感觉到其中的差异,然而这个改变为接下来的一系列扩展埋下了很好的伏笔。

概括来说,libnetwork所做的最核心事情是定义了一组标准的容器网络模型(Container Network Model,简称CNM),只要符合这个模型的网络接口就能被用于容器之间通信,而通信的过程和细节可以完全由网络接口来实现。

Docker的容器网络模型最初是由思科公司员工Erik提出的设想,比较有趣的是Erik本人并不是Docker和libnetwork代码的直接贡献者。最初Erik只是为了扩展Docker网络方面的能力,设计了一个Docker网桥的扩展原型,并将这个思路反馈给了Docker社区。然而他的大胆设想得到了Docker团队的认同,并在与Docker的其他合作伙伴广泛讨论之后,逐渐形成了libnetwork的雏形。

在这个网络模型中定义了三个的术语:SandboxEndpointNetwork

docker1.9.1

如上图所示,它们分别是容器通信中 『容器网络环境』『容器虚拟网卡』『主机虚拟网卡/网桥』 的抽象。

  • Sandbox:对应一个容器中的网络环境,包括相应的网卡配置、路由表、DNS配置等。CNM很形象的将它表示为网络的『沙盒』,因为这样的网络环境是随着容器的创建而创建,又随着容器销毁而不复存在的;

  • Endpoint:实际上就是一个容器中的虚拟网卡,在容器中会显示为eth0、eth1依次类推;

  • Network:指的是一个能够相互通信的容器网络,加入了同一个网络的容器直接可以直接通过对方的名字相互连接。它的实体本质上是主机上的虚拟网卡或网桥。

这种抽象为Docker的1.7版本带来了十分平滑的过渡,除了文档中的三种经典 『网络模式』 被换成了 『网络插件』,用户几乎感觉不到使用起来的差异。

直到1.9版本的到来,Docker终于将网络的控制能力完全开放给了终端用户,并因此改变了连接两个容器通信的操作方式(当然,Docker为兼容性做足了功夫,所以即便你不知道下面所有的这些差异点,仍然丝毫不会影响继续用过去的方式使用Docker)。

Docker 1.9中网络相关的变化集中体现在新的 docker network 命令上。

$ docker network --help
Usage:  docker network [OPTIONS]COMMAND [OPTIONS]
Commands:
 ls                       List all networks
 rm                       Remove a network
 create                   Create a network
 connect                  Connect container to anetwork
 disconnect               Disconnect container from anetwork
 inspect                  Display detailed networkinformation

简单介绍一下这些命令的作用。

1. docker network ls

这个命令用于列出所有当前主机上或Swarm集群上的网络:

$ docker network ls
NETWORK ID          NAME                DRIVER
6e6edc3eee42        bridge              bridge
1caa9a605df7        none                null
d34a6dec29d3        host                host

在默认情况下会看到三个网络,它们是Docker Deamon进程创建的。它们实际上分别对应了Docker过去的三种 『网络模式』

  • bridge:容器使用独立网络Namespace,并连接到docker0虚拟网卡(默认模式)
  • none:容器没有任何网卡,适合不需要与外部通过网络通信的容器
  • host:容器与主机共享网络Namespace,拥有与主机相同的网络设备

在引入libnetwork后,它们不再是固定的『网络模式』了,而只是三种不同『网络插件』的实体。说它们是实体,是因为现在用户可以利用Docker的网络命令创建更多与默认网络相似的网络,每一个都是特定类型网络插件的实体。

2. docker network create / docker network rm

这两个命令用于新建或删除一个容器网络,创建时可以用『–driver』参数使用的网络插件,例如:

$ docker network create --driver=bridge br0
b6942f95d04ac2f0ba7c80016eabdbce3739e4dc4abd6d3824a47348c4ef9e54

现在这个主机上有了一个新的bridge类型的Docker网络:

$ docker network ls
NETWORK ID          NAME                DRIVER
b6942f95d04a        br0                 bridge
...

Docker容器可以在创建时通过--net参数指定所使用的网络,连接到同一个网络的容器可以直接相互通信。

当一个容器网络不再需要时,可以将它删除:

$ docker network rm br0

3. docker network connect / docker network disconnect

这两个命令用于动态的将容器添加进一个已有网络,或将容器从网络中移除。为了比较清楚的说明这一点,我们来看一个例子。

参照前面的libnetwork容器网络模型示意图中的情形创建两个网络:

$ docker network create --driver=bridge frontend
$ docker network create --driver=bridge backend

然后运行三个容器,让第一个容器接入frontend网络,第二个容器同时接入两个网络,三个容器只接入backend网络。首先用--net参数可以很容易创建出第一和第三个容器:

$ docker run -td --name ins01 --net frontendindex.alauda.cn/library/busybox
$ docker run -td --name ins03 --net backendindex.alauda.cn/library/busybox

如何创建一个同时加入两个网络的容器呢?由于创建容器时的--net参数只能指定一个网络名称,因此需要在创建过后再用docker network connect命令添加另一个网络:

$ docker run -td --name ins02 --net frontendindex.alauda.cn/library/busybox
$ docker network connect backend ins02

现在通过ping命令测试一下这几个容器之间的连通性:

$ docker exec -it ins01 ping ins02
可以连通
$ docker exec -it ins01 ping ins03
找不到名称为ins03的容器
$ docker exec -it ins02 ping ins01
可以连通
$ docker exec -it ins02 ping ins03
可以连通
$ docker exec -it ins03 ping ins01
找不到名称为ins01的容器
$ docker exec -it ins03 ping ins02
可以连通

这个结果也证实了在相同网络中的两个容器可以直接使用名称相互找到对方,而在不同网络中的容器直接是不能够直接通信的。此时还可以通过docker networkdisconnect动态的将指定容器从指定的网络中移除:

$ docker network disconnect backend ins02
$ docker exec -it ins02 ping ins03
找不到名称为ins03的容器

可见,将ins02容器实例从backend网络中移除后,它就不能直接连通ins03容器实例了。

4. docker network inspect

最后这个命令可以用来显示指定容器网络的信息,以及所有连接到这个网络中的容器列表:

$ docker network inspect bridge
[{
   "Name":"bridge",
   "Id": "6e6edc3eee42722df8f1811cfd76d7521141915b34303aa735a66a6dc2c853a3",
   "Scope": "local",
   "Driver":"bridge",
   "IPAM": {
       "Driver":"default",
       "Config": [{"Subnet": "172.17.0.0/16"}]
   },
   "Containers": {
       "3d77201aa050af6ec8c138d31af6fc6ed05964c71950f274515ceca633a80773":{
           "EndpointID":"0751ceac4cce72cc11edfc1ed411b9e910a8b52fd2764d60678c05eb534184a4",
           "MacAddress":"02:42:ac:11:00:02",
          "IPv4Address": "172.17.0.2/16",
           "IPv6Address":""
       }
   },
...

值得指出的是,同一主机上的每个不同网络分别拥有不同的网络地址段,因此同时属于多个网络的容器会有多个虚拟网卡和多个IP地址。

由此可以看出,libnetwork带来的最直观变化实际上是:docker0不再是唯一的容器网络了,用户可以创建任意多个与docker0相似的网络来隔离容器之间的通信。 然而,要仔细来说,用户自定义的网络和默认网络还是有不一样的地方。

  • 默认的三个网络是不能被删除的,而用户自定义的网络可以用docker networkrm命令删掉;
  • 连接到默认的bridge网络连接的容器需要明确的在启动时使用--link参数相互指定,才能在容器里使用容器名称连接到对方。而连接到自定义网络的容器,不需要任何配置就可以直接使用容器名连接到任何一个属于同一网络中的容器。这样的设计即方便了容器之间进行通信,又能够有效限制通信范围,增加网络安全性;
  • 在Docker 1.9文档中已经明确指出,不再推荐容器使用默认的bridge网卡,它的存在仅仅是为了兼容早期设计。而容器间的--link通信方式也已经被标记为过时的功能,并可能会在将来的某个版本中被彻底移除。

Docker的内置Overlay网络

内置跨主机的网络通信一直是Docker备受期待的功能,在1.9版本之前,社区中就已经有许多第三方的工具或方法尝试解决这个问题,例如Macvlan、Pipework、Flannel、Weave等。虽然这些方案在实现细节上存在很多差异,但其思路无非分为两种:二层VLAN网络和Overlay网络

简单来说,二层VLAN网络的解决跨主机通信的思路是把原先的网络架构改造为互通的大二层网络,通过特定网络设备直接路由,实现容器点到点的之间通信。这种方案在传输效率上比Overlay网络占优,然而它也存在一些固有的问题。

  • 这种方法需要二层网络设备支持,通用性和灵活性不如后者;
  • 由于通常交换机可用的VLAN数量都在4000个左右,这会对容器集群规模造成限制,远远不能满足公有云或大型私有云的部署需求;
  • 大型数据中心部署VLAN,会导致任何一个VLAN的广播数据会在整个数据中心内泛滥,大量消耗网络带宽,带来维护的困难。

相比之下,Overlay网络是指在不改变现有网络基础设施的前提下,通过某种约定通信协议,把二层报文封装在IP报文之上的新的数据格式。这样不但能够充分利用成熟的IP路由协议进程数据分发,而且在Overlay技术中采用扩展的隔离标识位数,能够突破VLAN的4000数量限制,支持高达16M的用户,并在必要时可将广播流量转化为组播流量,避免广播数据泛滥。因此,Overlay网络实际上是目前最主流的容器跨节点数据传输和路由方案。

在Docker的1.9中版本中正式加入了官方支持的跨节点通信解决方案,而这种内置的跨节点通信技术正是使用了Overlay网络的方法。

说到Overlay网络,许多人的第一反应便是:低效,这种认识其实是带有偏见的。Overlay网络的实现方式可以有许多种,其中IETF(国际互联网工程任务组)制定了三种Overlay的实现标准,分别是:虚拟可扩展LAN(VXLAN)、采用通用路由封装的网络虚拟化(NVGRE)和无状态传输协议(SST),其中以VXLAN的支持厂商最为雄厚,可以说是Overlay网络的事实标准。

而在这三种标准以外还有许多不成标准的Overlay通信协议,例如Weave、Flannel、Calico等工具都包含了一套自定义的Overlay网络协议(Flannel也支持VXLAN模式),这些自定义的网络协议的通信效率远远低于IETF的标准协议[5],但由于他们使用起来十分方便,一直被广泛的采用而造成了大家普遍认为Overlay网络效率低下的印象。然而,根据网上的一些测试数据来看,采用VXLAN的网络的传输速率与二层VLAN网络是基本相当的。

解除了这些顾虑后,一个好消息是,Docker内置的Overlay网络是采用IETF标准的VXLAN方式,并且是VXLAN中普遍认为最适合大规模的云计算虚拟化环境的SDN Controller模式。

到目前为止一切都是那么美好,大家是不是想动手尝试一下了呢?

且慢,待我先稍泼些冷水。在许多的报道中只是简单的提到,这一特性的关键就是Docker新增的overlay类型网卡,只需要用户用docker networkcreate命令创建网卡时指定--driver=overlay参数就可以。看起来就像这样:

docker network create --driver=overlay ovr0

但现实的情况是,直到目前为止,Docker的Overlay网络功能与其Swarm集群是紧密整合的,因此为了使用Docker的内置跨节点通信功能,也就必须采纳Swarm作为集群的解决方案。这也是为什么Docker 1.9会与Swarm1.0同时发布,并标志着Swarm已经Product-Ready。此外,还有一些附加的条件:

  • 所有Swarm节点的Linux系统内核版本不低于3.16
  • 需要一个额外的配置存储服务,例如Consul、Etcd或ZooKeeper
  • 所有的节点都能够正常连接到配置存储服务的IP和端口
  • 所有节点运行的Docker后台进程需要使用--cluster-store-–cluster-advertise参数指定所使用的配置存储服务地址

我们先不解释为什么必须使用Swarm,稍后大家很快就会发现原因。假设上述条件的1和3都是满足的,接下来就需要建立一个外部配置存储服务,为了简便起见暂不考虑高可用性,可以采用单点的服务。

以Consul为例,用Docker来启动它,考虑到国内访问Docker Hub比较慢,建议采用灵雀云的Docker镜像仓库:

$ docker run -d \
   --restart="always" \
   --publish="8500:8500" \
   --hostname="consul" \
   --name="consul" \
  index.alauda.cn/sequenceiq/consul:v0.5.0-v6 -server -bootstrap

如果使用Etcd,可以用下面的命令启动容器,同样可以用『灵雀云』的Docker镜像仓库:

$ docker run -d \
   --restart="always" \
  --publish="2379:2379" \
   --name="etcd" \
  index.alauda.cn/googlecontainer/etcd:2.2.1 etcd \
   -name etcd0-advertise-client-urls http://<Etcd所在主机IP>:2379 \
   -listen-client-urlshttp://0.0.0.0:2379 -initial-cluster-state new

然后修改每个主机Docker后台进程启动脚本里的DOCKER_OPTS变量内容,如果是Consul加上下面这两项:

--cluster-store=consul://<Consul所在主机IP>:8500 -–cluster-advertise=eth1:2376

如果是Etcd则加上:

--cluster-store=etcd://<Etcd所在主机IP>:2379/store-–cluster-advertise=eth1:2376

然后重启每个主机的Docker后台进程,一切准备就绪。当然,由于修改和重启Docker后台进程本身是比较麻烦的事情,如果用户业务可能会使用到跨节点网络通信,建议在架设Docker集群的时候就事先准备配置存储服务,然后直接在添加主机节点时就可以将相应参数加入到Docker的启动配置中了。

至于配置存储服务的运行位置,通常建议是与运行业务容器的节点分开,使用独立的服务节点,这样才能确保所有运行业务容器的节点是无状态的,可以被平等的调度和分配运算任务。

接下来到了创建Overlay网络的时候,问题来了,我们要建的这个网络是横跨所有节点的,也就是说在每个节点都应该有一个名称、ID和属性完全一致的网络,它们之间还要相互认可对方为自己在不同节点的副本。如何实现这种效果呢?目前的Docker network命令还无法做到,因此只能借助于Swarm。

构建Swarm集群的方法在这里不打算展开细说,只演示一下操作命令。为了简便起见,我们使用Swarm官方的公有token服务作为节点组网信息的存储位置,首先在任意节点上通过以下命令获取一个token:

$ docker run --rm swarm create
6856663cdefdec325839a4b7e1de38e8

任意选择其中一个节点作为集群的Master节点,并在主机上运行Swarm Master服务:

$ docker run -d -p 3375:2375 swarm manage token://<前面获得的token字符串>

在其他作为Docker业务容器运行的节点上运行Swarm Agent服务:

$ docker run -d swarm join --addr=<当前主机IP>:2375token://<前面获得的token字符串>

这样便获得了一个Swarm的集群。当然,我们也可以利用前面已经建立的Consul或Etcd服务替代官方的token服务,只需稍微修改启动参数即可,具体细节可以参考Swarm的文档。

Swarm提供与Docker服务完全兼容的API,因此可以直接使用docker命令进行操作。注意上面命令中创建Master服务时指定的外部端口号3375,它就是用来连接Swarm服务的地址。现在我们就可以创建一个Overlay类型的网络了:

$ docker -H tcp://<Master节点地址>:3375network create --driver=overlay ovr0

这个命令被发送给了Swarm服务,Swarm会在所有Agent节点上添加一个属性完全相同的Overlay类型网络。也就是说,现在任意一个Agent节点上执行docker networkls命令都能够看到它,并且使用docker network inspect命令查看它的信息时,将在每个节点上获得完全相同的内容。通过Docker连接到Swarm集群执行network ls命令就可以看到整个集群网络的全貌:

$ docker -H tcp://<Master节点地址>:3375network ls
$ docker network ls
NETWORK ID      NAME                   DRIVER
445ede8764da   swarm-agent-1/bridge   bridge
2b9c1c73cc5f   swarm-agent-2/bridge   bridge
...
90f6666a9c5f    ovr0                   overlay

在Swarm的网络里面,每个网络的名字都会加上节点名称作为前缀,但Overlay类型的网络是没有这个前缀的,这也说明了这类网络是被所有节点共有的。

下面我们在Swarm中创建两个连接到Overlay网络的容器,并用Swarm的过滤器限制这两个容器分别运行在不同的节点上。

$ docker -H tcp://<Master节点地址>:3375 run-td --name ins01 --net ovr0 --env="constraint:node==swarm-agent-1"index.alauda.cn/library/busybox
$ docker -H tcp://<Master节点地址>:3375 run-td --name ins02 --net ovr0 --env="constraint:node==swarm-agent-2"index.alauda.cn/library/busybox

然后从ins01容器尝试连接ins02容器:

$ docker -H tcp://<Master节点地址>:3375 exec-it ins01 ping ins02
可以连通

至此,我们就已经在Docker的Overlay网络上成功的进行了跨节点的数据通信。

想简单点?用灵雀云吧

不知大家发现了么有,不论是过去的Pipework、Flannel、Weave方式,还是Docker 1.9内置的Overlay网络,尽管所有的这些方案都宣传自己足够的简单易用,构建跨节点通信的容器依然是一件不得已而为之的事情。

之所以这样说,是因为在现实应用场景中往往由于服务直接错综复杂的连接,需要相互通信的容器数量远远的超过的单个主机节点所能够承受的容量,这才使得我们不得不在现有的基础实施上自行维护一套服务机制,将容器间的通信扩展到多个节点上。但维护这些跨节点通信基础设施本身是不为企业带来实质的业务价值的!

在当下,云基础设施迅速发展的大环境已经为许多企业创造了弯道超车的机遇:通过采用云平台,企业省去了自己购置和组建大规模网络和计算资源以及管理庞大运维团队的投入,只需专注于业务本身的创意和设计就能收获巨大的利润。与此同时,容器作为新一代的服务部署和调度工具被越来越广泛的采用,而解决容器通信问题本不应该成为企业需要关注的要点,现在却成为扩大服务规模时横在眼前无法忽视的阻碍。

灵雀云作为容器云服务平台中的佼佼者,为容器的使用者屏蔽了容器运行节点的细节,使得用户完全感觉不到跨节点通信所带来的差异。那么如何在灵雀云中运行两个容器,并使之相互通信呢?

首先安装灵雀的alauda命令行工具,使用alauda login命令登陆。

$ alauda login
Username: fanlin
Password: *灵雀密码*
[alauda] Successfully logged in as fanlin.
[alauda] OK

然后运行两个容器,前者运行Nginx服务,后者用于测试与前者的连接。

$ alauda service run --publish=80/http web index.alauda.cn/library/nginx:1.9.9
[alauda] Creating and starting service "web"
[alauda] OK
$ alauda service run client index.alauda.cn/library/busybox:latest
[alauda] Creating and starting service "client"
[alauda] OK

尝试从client容器实例访问web容器实例提供的HTTP服务,注意灵雀云自动生成的地址格式。

$ alauda service exec client wget -O- http://web-fanlin.myalauda.cn
Password for fanlin@exec.alauda.cn:
...
<html>
<head>
<title>Welcome to nginx!</title>
<style>
...

可以看到返回了Nginx默认首页的内容,证明这两个容器之间可以连通。除了建立单个的任务,还可以使用alauda compose命令快速建立多个容器组成的服务集合。

那么着两个容器分别是运行在哪个主机上的?管他呢,灵雀云已经将这些底层细节统统藏起来了,所以只管往云上继续创建更多的服务吧,再也不用担心主机资源耗尽,面对跨节点怎么通信的问题啦。

作者简介:林帆,生在80后尾巴的IT攻城狮,ThoughtWorks成都办公室CloudOps小组成员,平时喜欢在业余时间研究DevOps相关的应用,目前在备考AWS认证和推广Docker相关技术。

jack.zh 标签:Docker 继续阅读

4 ℃

2471 ℉

15.10.30

Python的优雅技巧(不断添加)

枚举

不要这么做:

i = 0 
for item in iterable: 
    print i, item 
    i += 1

而是这样:

for i, item in enumerate(iterable):    
    print i, item

Enumerate可以接受第二个参数,例如:

>>> list(enumerate('abc')) 
[(0, 'a'), (1, 'b'), (2, 'c')]
>>> list(enumerate('abc', 1)) 
[(1, 'a'), (2, 'b'), (3, 'c')]

字典/集合 解析

你可能知道列表解析,但不知道字典/集合解析。字典/集合解析简单而且高效,例如:

my_dict = {i: i * i for i in xrange(100)} 
my_set = {i * 15 for i in xrange(100)}

#There is only a difference of ':' in both

浮点数除法

如果我们除以一个整数,即使结果是一个浮点数,Python(2) 依旧会给我们一个整数。为了规避这个问题,我们需要这样做:

result = 1.0/2

但是现在有一种别的方法可以解决这个问题,甚至在之前我都没有意识到有这种方法存在。你可以进行如下操作:

from __future__ import division 
result = 1/2

#print(result)
#0.5

需要注意的是这个窍门只适用于Python 2。在Python 3 中就不需要进行import 操作了,因为它已经默认进行import了。

简单的服务器

你想快速简单的分享目录下的文件吗?可以这样做:

#Python2
python -m SimpleHTTPServer

#Python 3
python3 -m http.server

这回启动一个服务器

Python表达式求值

我们都知道eval,但也许并不是所有人都知道literal_eval.可以这么做:

import ast 
my_list = ast.literal_eval(expr)

而不是这样:

expr = "[1, 2, 3]" 
my_list = eval(expr)

我相信对于大多数人来说这种形式是第一次看见,但是实际上这个在Python中已经存在很长时间了。

分析脚本

按下面的方式运行脚本,可以很简单的对其进行分析:

python -m cProfile my_script.py

对象自检

在Python中,可以通过dir()来检查对象,例如:

>>> foo = [1, 2, 3, 4]
>>> dir(foo) 
['__add__', '__class__', '__contains__', 
'__delattr__', '__delitem__', '__delslice__', ... , 
'extend', 'index', 'insert', 'pop', 'remove', 
'reverse', 'sort']

调试脚本

你可以使用pdb模块在脚本中设置断点来调试脚本,就像这样:

import pdb
pdb.set_trace()

你可以在脚本的任何地方加入pdb.set_trace(),该函数会在那个位置设置一个断点。超级方便。你应该多阅读pdb 函数的相关内容,因为在它里面还有很多鲜为人知的功能。

简化if结构

如果必须检查一些值,可以用

if n in [1,4,5,6]:

而不是用复杂的if结构:

if n==1 or n==4 or n==5 or n==6:

字符串/数列 逆序

下面的方式可以快速反转一个列表:

>>> a = [1,2,3,4]
>>> a[::-1]
[4, 3, 2, 1]

#This creates a new reversed list. 
#If you want to reverse a list in place you can do:

a.reverse()

这种方式同样适用于字符串:

>>> foo = "yasoob"
>>> foo[::-1]
'boosay'

优雅地打印

下面的方式可以用优雅的方式打印字典和列表:

from pprint import pprint 
pprint(my_dict)

这用于字典打印是非常高效的,如果你想从文件中快速优雅的打印出json,可以这样做:

cat file.json | python -m json.tools

三元运算

三元运算是if-else 语句的快捷操作,也被称为条件运算。这里有几个例子可以供你参考:

[on_true] if [expression] else [on_false]
x, y = 50, 25
small = x if x < y else y

优化算法时间复杂度

算法的时间复杂度对程序的执行效率影响最大,在Python中可以通过选择合适的数据结构来优化时间复杂度,如list和set查找某一个元素的时间复杂度分别是O(n)和O(1)。不同的场景有不同的优化方式,总得来说,一般有分治,分支界限,贪心,动态规划等思想。

减少冗余数据

如用上三角或下三角的方式去保存一个大的对称矩阵。在0元素占大多数的矩阵里使用稀疏矩阵表示。

合理使用copy与deepcopy

对于dict和list等数据结构的对象,直接赋值使用的是引用的方式。而有些情况下需要复制整个对象,这时可以使用copy包里的copy和deepcopy,这两个函数的不同之处在于后者是递归复制的。效率也不一样:(以下程序在ipython中运行)

import copy
a = range(100000)
%timeit -n 10 copy.copy(a) # 运行10次 copy.copy(a)
%timeit -n 10 copy.deepcopy(a)
10 loops, best of 3: 1.55 ms per loop
10 loops, best of 3: 151 ms per loop

timeit后面的-n表示运行的次数,后两行对应的是两个timeit的输出,下同。由此可见后者慢一个数量级。

使用dict或set查找元素

python dict和set都是使用hash表来实现(类似c++11标准库中unordered_map),查找元素的时间复杂度是O(1)

a = range(1000)
s = set(a)
d = dict((i,1) for i in a)
%timeit -n 10000 100 in d
%timeit -n 10000 100 in s
10000 loops, best of 3: 43.5 ns per loop
10000 loops, best of 3: 49.6 ns per loop

dict`的效率略高(占用的空间也多一些)。

合理使用生成器(generator)和yield

%timeit -n 100 a = (i for i in range(100000))
%timeit -n 100 b = [i for i in range(100000)]
100 loops, best of 3: 1.54 ms per loop
100 loops, best of 3: 4.56 ms per loop

使用()得到的是一个generator对象,所需要的内存空间与列表的大小无关,所以效率会高一些。在具体应用上,比如set(i for i in range(100000))会比set([i for i in range(100000)])快。

但是对于需要循环遍历的情况:

%timeit -n 10 for x in (i for i in range(100000)): pass
%timeit -n 10 for x in [i for i in range(100000)]: pass
10 loops, best of 3: 6.51 ms per loop
10 loops, best of 3: 5.54 ms per loop

后者的效率反而更高,但是如果循环里有break,用generator的好处是显而易见的。yield也是用于创建generator:

def yield_func(ls):
    for i in ls:
        yield i+1

def not_yield_func(ls):
    return [i+1 for i in ls]

ls = range(1000000)
%timeit -n 10 for i in yield_func(ls):pass
%timeit -n 10 for i in not_yield_func(ls):pass
10 loops, best of 3: 63.8 ms per loop
10 loops, best of 3: 62.9 ms per loop

对于内存不是非常大的list,可以直接返回一个list,但是可读性yield更佳(人个喜好)。

python2.x内置generator功能的有xrange函数、itertools包等。

优化循环

循环之外能做的事不要放在循环内,比如下面的优化可以快一倍:

a = range(10000)
size_a = len(a)
%timeit -n 1000 for i in a: k = len(a)
%timeit -n 1000 for i in a: k = size_a
1000 loops, best of 3: 569 µs per loop
1000 loops, best of 3: 256 µs per loop

优化包含多个判断表达式的顺序

对于and,应该把满足条件少的放在前面,对于or,把满足条件多的放在前面。如:

a = range(2000)  
%timeit -n 100 [i for i in a if 10 &lt; i &lt; 20 or 1000 &lt; i &lt; 2000]
%timeit -n 100 [i for i in a if 1000 &lt; i &lt; 2000 or 100 &lt; i &lt; 20]     
%timeit -n 100 [i for i in a if i % 2 == 0 and i &gt; 1900]
%timeit -n 100 [i for i in a if i &gt; 1900 and i % 2 == 0]
100 loops, best of 3: 287 µs per loop
100 loops, best of 3: 214 µs per loop
100 loops, best of 3: 128 µs per loop
100 loops, best of 3: 56.1 µs per loop

使用join合并迭代器中的字符串

In [1]: %%timeit
   ...: s = ''
   ...: for i in a:
   ...:         s += i
   ...:
10000 loops, best of 3: 59.8 µs per loop

In [2]: %%timeit
s = ''.join(a)
   ...:
100000 loops, best of 3: 11.8 µs per loop

join对于累加的方式,有大约5倍的提升。

选择合适的格式化字符方式

s1, s2 = 'ax', 'bx'
%timeit -n 100000 'abc%s%s' % (s1, s2)
%timeit -n 100000 'abc{0}{1}'.format(s1, s2)
%timeit -n 100000 'abc' + s1 + s2
100000 loops, best of 3: 183 ns per loop
100000 loops, best of 3: 169 ns per loop
100000 loops, best of 3: 103 ns per loop

三种情况中,%的方式是最慢的,但是三者的差距并不大(都非常快)。(个人觉得%的可读性最好)

不借助中间变量交换两个变量的值

In [3]: %%timeit -n 10000
    a,b=1,2
   ....: c=a;a=b;b=c;
   ....:
10000 loops, best of 3: 172 ns per loop

In [4]: %%timeit -n 10000
a,b=1,2
a,b=b,a
   ....:
10000 loops, best of 3: 86 ns per loop

使用a,b=b,a而不是c=a;a=b;b=c;来交换a,b的值,可以快1倍以上。

使用if is

a = range(10000)
%timeit -n 100 [i for i in a if i == True]
%timeit -n 100 [i for i in a if i is True]
100 loops, best of 3: 531 µs per loop
100 loops, best of 3: 362 µs per loop

使用 if is Trueif == True 将近快一倍。

使用级联比较x &lt; y &lt; z

x, y, z = 1,2,3
%timeit -n 1000000 if x &lt; y &lt; z:pass
%timeit -n 1000000 if x &lt; y and y &lt; z:pass
1000000 loops, best of 3: 101 ns per loop
1000000 loops, best of 3: 121 ns per loop

x > y > z效率略高,而且可读性更好。

while 1while True 更快

def while_1():
    n = 100000
    while 1:
        n -= 1
        if n &lt;= 0: break
def while_true():
    n = 100000
    while True:
        n -= 1
        if n &lt;= 0: break    

m, n = 1000000, 1000000 
%timeit -n 100 while_1()
%timeit -n 100 while_true()
100 loops, best of 3: 3.69 ms per loop
100 loops, best of 3: 5.61 ms per loop

while 1 比 while true快很多,原因是在python2.x中,True是一个全局变量,而非关键字。

使用**而不是pow

%timeit -n 10000 c = pow(2,20)
%timeit -n 10000 c = 2**20
10000 loops, best of 3: 284 ns per loop
10000 loops, best of 3: 16.9 ns per loop

**就是快10倍以上!

## 使用 cProfile, cStringIO 和 cPickle等用c实现相同功能(分别对应profile, StringIO, pickle)的包

import cPickle
import pickle
a = range(10000)
%timeit -n 100 x = cPickle.dumps(a)
%timeit -n 100 x = pickle.dumps(a)
100 loops, best of 3: 1.58 ms per loop
100 loops, best of 3: 17 ms per loop

由c实现的包,速度快10倍以上!

使用最佳的反序列化方式

下面比较了eval, cPickle, json方式三种对相应字符串反序列化的效率:

import json
import cPickle
a = range(10000)
s1 = str(a)
s2 = cPickle.dumps(a)
s3 = json.dumps(a)
%timeit -n 100 x = eval(s1)
%timeit -n 100 x = cPickle.loads(s2)
%timeit -n 100 x = json.loads(s3)
100 loops, best of 3: 16.8 ms per loop
100 loops, best of 3: 2.02 ms per loop
100 loops, best of 3: 798 µs per loop

可见json比cPickle快近3倍,比eval快20多倍。

使用C扩展(Extension)

目前主要有CPython(python最常见的实现的方式)原生API, ctypes,Cython,cffi三种方式,它们的作用是使得Python程序可以调用由C编译成的动态链接库,其特点分别是:

CPython原生API: 通过引入Python.h头文件,对应的C程序中可以直接使用Python的数据结构。实现过程相对繁琐,但是有比较大的适用范围。

ctypes: 通常用于封装(wrap)C程序,让纯Python程序调用动态链接库(Windows中的dll或Unix中的so文件)中的函数。如果想要在python中使用已经有C类库,使用ctypes是很好的选择,有一些基准测试下,python2+ctypes是性能最好的方式。

Cython: Cython是CPython的超集,用于简化编写C扩展的过程。Cython的优点是语法简洁,可以很好地兼容numpy等包含大量C扩展的库。Cython的使得场景一般是针对项目中某个算法或过程的优化。在某些测试中,可以有几百倍的性能提升。

cffi: cffi的就是ctypes在pypy(详见下文)中的实现,同进也兼容CPython。cffi提供了在python使用C类库的方式,可以直接在python代码中编写C代码,同时支持链接到已有的C类库。

使用这些优化方式一般是针对已有项目性能瓶颈模块的优化,可以在少量改动原有项目的情况下大幅度地提高整个程序的运行效率。

并行编程

因为GIL的存在,Python很难充分利用多核CPU的优势。但是,可以通过内置的模块multiprocessing实现下面几种并行模式:

多进程:对于CPU密集型的程序,可以使用multiprocessing的Process,Pool等封装好的类,通过多进程的方式实现并行计算。但是因为进程中的通信成本比较大,对于进程之间需要大量数据交互的程序效率未必有大的提高。

多线程:对于IO密集型的程序,multiprocessing.dummy模块使用multiprocessing的接口封装threading,使得多线程编程也变得非常轻松(比如可以使用Pool的map接口,简洁高效)。

分布式:multiprocessing中的Managers类提供了可以在不同进程之共享数据的方式,可以在此基础上开发出分布式的程序。

不同的业务场景可以选择其中的一种或几种的组合实现程序性能的优化。

终级大杀器:PyPy

PyPy是用RPython(CPython的子集)实现的Python,根据官网的基准测试数据,它比CPython实现的Python要快6倍以上。快的原因是使用了Just-in-Time(JIT)编译器,即动态编译器,与静态编译器(如gcc,javac等)不同,它是利用程序运行的过程的数据进行优化。由于历史原因,目前pypy中还保留着GIL,不过正在进行的STM项目试图将PyPy变成没有GIL的Python。

如果python程序中含有C扩展(非cffi的方式),JIT的优化效果会大打折扣,甚至比CPython慢(比Numpy)。所以在PyPy中最好用纯Python或使用cffi扩展。

随着STM,Numpy等项目的完善,相信PyPy将会替代CPython。

使用性能分析工具

除了上面在ipython使用到的timeit模块,还有cProfile。cProfile的使用方式也非常简单: python -m cProfile filename.pyfilename.py 是要运行程序的文件名,可以在标准输出中看到每一个函数被调用的次数和运行的时间,从而找到程序的性能瓶颈,然后可以有针对性地优化。

jack.zh 标签:Python 继续阅读

2784 ℉

15.10.09

Elixir入门教程<中>

7-键值列表-图-字典

到目前还没有讲到任何关联性数据结构,即那种可以将一个或几个值关联到一个key上。 不同语言有不同的叫法,如字典,哈希,关联数组,图,等等。

Elixir中有两种主要的关联性结构:键值列表(keyword list)和图(map)。

7.1-键值列表

在很多函数式语言中,常用二元元组的列表来表示关联性数据结构。 在Elixir中也是这样。当我们有了一个元组(不一定仅有两个元素的元组)的列表,并且每个元组的第一个元素是个原子, 那就称之为键值列表:

iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
iex> list[:a]
1

当原子key和关联的值之间没有逗号分隔时,可以把原子的冒号拿到字母的后面。这时,原子与后面的数值之间要有一个空格。

如你所见,Elixir使用比较特殊的语法来定义这样的列表,但实际上它们会映射到一个元组列表。 因为它们是简单的列表而已,所有针对列表的操作,键值列表也可以用。

比如,可以用++运算符为列表添加元素:

iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]

上面例子中重复出现了:a这个key,这是允许的。 以这个key取值时,取回来的是第一个找到的(因为有序):

iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0

键值列表十分重要,它有两大特点: - 有序 - key可以重复(!仔细看上面两个示例)

例如,Ecto库使用这两个特点写出了精巧的DSL(用来写数据库query):

query = from w in Weather,
          where: w.prcp > 0,
          where: w.temp < 20,
        select: w

这些特性使得键值列表成了Elixir中为函数提供额外选项的默认手段。 在第5章我们讨论了if/2宏,提到了下方的语法:

iex> if false, do: :this, else: :that
:that

do: 和else: 就是键值列表!事实上代码等同于:

iex> if(false, [do: :this, else: :that])
:that

当键值列表是函数最后一个参数时,方括号就成了可选的。

为了操作关键字列表,Elixir提供了键值(keyword)模块。 记住,键值列表就是简单的列表,和列表一样提供了线性的性能。列表越长,获取长度或找到一个键值的速度越慢。 因此,关键字列表在Elixir中一般就作为函数调用的可选项。 如果你要存储大量数据,并且保证一个键只对应最多一个值,那就使用图。

对键值列表做模式匹配:

iex> [a: a] = [a: 1]
[a: 1]
iex> a
1
iex> [a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
iex> [b: b, a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]

尽管如此,对列表使用模式匹配很少用到。因为不但要元素个数相等,顺序还要匹配。

7.2-图(maps)

无论何时想用键-值结构,图都应该是你的第一选择。Elixir中,用%{}定义图:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b

和键值列表对比,图有两主要区别: - 图允许任何类型值作为键 - 图的键没有顺序

如果你向图添加一个已有的键,将会覆盖之前的键-值对:

iex> %{1 => 1, 1 => 2}
%{1 => 2}

如果图中的键都是原子,那么你也可以用键值列表中的一些语法:

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}

对比键值列表,图的模式匹配很是有用:

iex> %{} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

如上所示,图A与另一个图B做匹配。图B中只要包含有图A的键,那么两个图就能匹配上。若图A是个空的,那么任意图B都能匹配上。 但是如果图B里不包含图A的键,那就匹配失败了。

图还有个有趣的功能:它提供了特殊的语法来修改和访问原子键:

iex> map = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> map.a
1
iex> %{map | :a => 2}
%{:a => 2, 2 => :b}
iex> %{map | :c => 3}
** (ArgumentError) argument error

使用上面两种语法要求的前提是所给的键是切实存在的。最后一条语句错误的原因就是键:c不存在。

未来几章中我们还将讨论结构体(structs)。结构体提供了编译时的保证,它是Elixir多态的基础。 结构体是基于图的,上面例子提到的修改键值的前提就变得十分重要。

图模块提供了许多关于图的操作。它提供了与键值列表许多相似的API,因为这两个数据结构都实现了字典的行为。

图是最近连同EEP 43被引入Erlang虚拟机的。 Erlang 17提供了EEP的部分实现,只支持_一小部分_图功能。 这意味着图仅在存储不多的键时,图的性能还行。为了解决这个问题,Elixir还提供了HashDict模块该模块提供了一个字典来支持大量的键,并且性能不错。

7.3-字典(Dicts)

Elixir中,键值列表和图都被称作字典。换句话说,一个字典就像一个接口(在Elixir中称之为行为behaviour)。 键值列表和图模块实现了该接口。

这个接口定义于Dict模块,该模块还提供了底层实现的一个API:

iex> keyword = []
[]
iex> map = %{}
%{}
iex> Dict.put(keyword, :a, 1)
[a: 1]
iex> Dict.put(map, :a, 1)
%{a: 1}

字典模块允许开发者实现他们自己的字典形式,提供一些特殊的功能,然后联系到现存的的Elixir代码中去。 字典模块还提供了所有字典类型都可以使用的函数。如,Dicr.equal?/2可以比较两个字典类型(可以是不同的实现)。

你会疑惑些程序时用keyword,Map还是Dict模块呢?答案是:看情况。

如果你的代码期望接受一个关键字作为参数,那么使用简直列表模块。如果你想操作一个图,那就使用图模块。 如果你想你的API对所有字典类型的实现都有用,那就使用字典模块(确保以不同的实现作为参数测试一下)。

8-模块

Elixir中我们把许多函数组织成一个模块。我们在前几章已经提到了许多模块,如String模块

iex> String.length "hello"
5

创建自己的模块,用defmodule宏。用def宏在其中定义函数:

iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3

像ruby一样,模块名大写起头

8.1-编译

通常把模块写进文件,这样可以编译和重用。假如文件math.ex有如下内容:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

这个文件可以用elixirc进行编译:

$ elixirc math.ex

这将生成名为Elixir.Math.beam的bytecode文件。 如果这时再启动iex,那么这个模块就已经可以用了(假如在含有该编译文件的目录启动iex):

iex> Math.sum(1, 2)
3

Elixir工程通常组织在三个文件夹里: - ebin,包括编译后的字节码 - lib,包括Elixir代码(.ex文件) - test,测试代码(.exs文件)

实际项目中,构建工具Mix会负责编译,并且设置好正确的路径。 而为了学习方便,Elixir也提供了脚本模式,可以更灵活而不用编译。

8.2-脚本模式

除了.ex文件,Elixir还支持.exs脚本文件。Elixir对两种文件一视同仁,唯一区别是.ex文件有待编译, 而.exs文件用来作脚本执行,不需要编译。例如,如下创建名为math.exs的文件:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)

执行之:

$ elixir math.exs

这中情况,文件将在内存中编译和执行,打印出“3”作为结果。没有比特码文件生成。 后文中(为了学习和练习方便),推荐使用脚本模式执行学到的代码。

8.3-命名函数

在某模块中,我们可以用def/2宏定义函数,用defp/2定义私有函数。 用def/2定义的函数可以被其它模块中的代码使用,而私有函数仅在定义它的模块内使用。

defmodule Math do
  def sum(a, b) do
    do_sum(a, b)
  end

  defp do_sum(a, b) do
    a + b
  end
end

Math.sum(1, 2)    #=> 3
Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

函数声明也支持使用卫兵或多个子句。 如果一个函数有好多子句,Elixir会匹配每一个子句直到找到一个匹配的。 下面例子检查参数是否是数字:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_number(x) do
    false
  end
end

Math.zero?(0)  #=> true
Math.zero?(1)  #=> false

Math.zero?([1,2,3])
#=> ** (FunctionClauseError)

如果没有一个子句能匹配参数,会报错。

8.4-函数捕捉

本教程中提到函数,都是用name/arity的形式描述。这种表示方法可以被用来获取一个命名函数(赋给一个函数型变量)。 下面用iex执行一下上文定义的math.exs文件:

$ iex math.exs
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function fun
true
iex> fun.(0)
true

&<function notation>从函数名捕捉一个函数,它本身代表该函数值(函数类型的值)。 它可以不必赋给一个变量,直接用括号来使用该函数。 本地定义的,或者已导入的函数,比如is_function/1,可以不前缀模块名:

iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true

这种语法还可以作为快捷方式使用与创建函数:

iex> fun = &(&1 + 1)
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> fun.(1)
2

&1 表示传给该函数的第一个参数。上面例子中,&(&1+1)其实等同于n x->x+1 end。 在创建短小函数时,这个很方便。想要了解更多关于&捕捉操作符,参考Kernel.SpecialForms文档

8.5-默认参数

Elixir中,命名函数也支持默认参数:

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

任何表达式都可以作为默认参数,但是只在函数调用时用到了才被执行(函数定义时,那些表达式只是存在那儿,不执行;函数调用时,没有用到默认值,也不执行)。

defmodule DefaultTest do
  def dowork(x \\ IO.puts "hello") do
    x
  end
end
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
hello
:ok

如果有默认参数值的函数有了多条子句,推荐先定义一个函数头(无具体函数体)仅为了声明这些默认值:

defmodule Concat do
  def join(a, b \\ nil, sep \\ " ")

  def join(a, b, _sep) when nil?(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

使用默认值时,注意对函数重载会有一定影响。考虑下面例子:

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep \\ " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

如果将以上代码保存在文件“concat.ex”中并编译,Elixir会报出以下警告:

concat.ex:7: this clause cannot match because a previous clause at line 2 always matches

编译器是在警告我们,在使用两个参数调用join函数时,总使用第一个函数定义。 只有使用三个参数调用时,才会使用第二个定义:

$ iex concat.exs
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"

后面几章将介绍使用命名函数来做循环,如何从别的模块中导入函数,以及模块的属性等。

9-递归

因为在Elixir中(或所有函数式语言中),数据有不变性(immutability),因此在写循环时与传统的命令式(imperative)语言有所不同。 例如某命令式语言的循环可以这么写:

for(i = 0; i < array.length; i++) {
  array[i] = array[i] * 2
}

上面例子中,我们改变了array,以及辅助变量i的值。这在Elixir中是不可能的。 尽管如此,函数式语言却依赖于某种形式的循环—递归:一个函数可以不断地被递归调用,直到某条件满足才停止。 考虑下面的例子:打印一个字符串若干次:

defmodule Recursion do
  def print_multiple_times(msg, n) when n <= 1 do
    IO.puts msg
  end

  def print_multiple_times(msg, n) do
    IO.puts msg
    print_multiple_times(msg, n - 1)
  end
end

Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!

一个函数可以有许多子句(上面看起来定义了两个函数,但卫兵条件不同,可以看作同一个函数的两个子句)。 当参数匹配该子句的模式,且该子句的卫兵表达式返回true,才会执行该子句内容。 上面例子中,当print_multiple_times/2第一次调用时,n的值是3。

第一个子句有卫兵表达式要求n必须小于等于1。因为不满足此条件,代码找该函数的下一条子句。

参数匹配第二个子句,且该子句也没有卫兵表达式,因此得以执行。 首先打印msg,然后调用自身并传递第二个参数n-1(=2)。 这样msg又被打印一次,之后调用自身并传递参数n-1(=1)。

这个时候,n满足第一个函数子句条件。遂执行该子句,打印msg,然后就此结束。

我们称例子中第一个函数子句这种子句为“基本情形”。 基本情形总是最后被执行,因为起初通常都不匹配执行条件,程序而转去执行其它子句。 但是,每执行一次其它子句,条件都向这基本情形靠拢一点,直到最终回到基本情形处执行代码。

下面我们使用递归的威力来计算列表元素求和:

defmodule Math do
  def sum_list([head|tail], accumulator) do
    sum_list(tail, head + accumulator)
  end

  def sum_list([], accumulator) do
    accumulator
  end
end

Math.sum_list([1, 2, 3], 0) #=> 6

我们首先用列表[1,2,3]和初值0作为参数调用函数,程序将逐个匹配各子句的条件,执行第一个符合要求的子句。 于是,参数首先满足例子中定义的第一个子句。参数匹配使得head = 1,tail = [2,3],accumulator = 0。

然后执行该字据内容,把head + accumulator作为第二个参数,连带去掉了head的列表做第一个参数,再次调用函数本身。 如此循环,每次都把新传入的列表的head加到accumulator上,传递去掉了head的列表。 最终传递的列表为空,符合第二个子句的条件,执行该子句,返回accumulator的值6。

几次函数调用情况如下:

sum_list [1, 2, 3], 0
sum_list [2, 3], 1
sum_list [3], 3
sum_list [], 6

这种使用列表做参数,每次削减一点列表的递归方式,称为“递减”算法,是函数式编程的核心。
如果我们想给每个列表元素加倍呢?:

defmodule Math do
  def double_each([head|tail]) do
    [head * 2| double_each(tail)]
  end

  def double_each([]) do
    []
  end
end

Math.double_each([1, 2, 3]) #=> [2, 4, 6]

此处使用了递归来遍历列表元素,使它们加倍,然后返回新的列表。 这样以列表为参数,递归处理其每个元素的方式,称为“映射(map)”算法。

递归和列尾调用优化(tail call optimization)是Elixir中重要的部分,通常用来创建循环。 尽管如此,在Elixir中你很少会使用以上方式来递归地处理列表。

下一章要介绍的Enum模块为操作列表提供了诸多方便。 比如,下面例子:

iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end)
[2, 4, 6]

或者,使用捕捉的语法:

iex> Enum.reduce([1, 2, 3], 0, &+/2)
6
iex> Enum.map([1, 2, 3], &(&1 * 2))
[2, 4, 6]

10-枚举类型和流

10.1-枚举类型

Elixir提供了枚举类型(enumerables)的概念,使用[Enum模块]()操作它们。我们已经介绍过两种枚举类型:列表和图。

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

Enum模块为枚举类型提供了大量函数来变化,排序,分组,过滤和读取元素。 Enum模块是开发者最常用的模块之一。

Elixir还提供了范围(range):

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6

因为Enum模块在设计时为了适用于不同的数据类型,所以它的API被限制为多数据类型适用的函数。 为了实现某些操作,你可能需要针对某类型使用某特定的模块。 比如,如果你要在列表中某特定位置插入一个元素,要用List模块中的List.insert_at/3函数。而向某些类型内插入数据是没意义的,比如范围。

Enum中的函数是多态的,因为它们能处理不同的数据类型。 尤其是,模块中可以适用于不同数据类型的函数,它们是遵循了Enumerable协议。 我们在后面章节中将讨论这个协议。下面将介绍一种特殊的枚举类型:流。

10.2-积极vs懒惰

Enum模块中的所有函数都是积极的。多数函数接受一个枚举类型,并返回一个列表:

iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.filter(1..3, odd?)
[1, 3]

这意味着当使用Enum进行多种操作时,每次操作都生成一个中间列表,直到得出最终结果:

iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

上面例子是一个含有多个操作的管道。从一个范围开始,然后给每个元素乘以3。 该操作将会生成的中间结果是含有100000个元素的列表。 然后我们过滤掉所有偶数,产生又一个新中间结果:一个50000元素的列表。 最后求和,返回结果。 >这个符号的用法似乎和F#中的不一样啊…

作为一个替代,流模块提供了懒惰的实现:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

与之前Enum的处理不同,流先创建了一系列的计算操作。然后仅当我们把它传递给Enum模块,它才会被调用。流这种方式适用于处理大量的(甚至是无限的)数据集合。

10.3-流

流是懒惰的,比起Enum来说。 分步分析一下上面的例子,你会发现流与Enum的区别:

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

流操作返回的不是结果列表,而是一个数据类型—流,一个表示要对范围1..100000使用map操作的动作。

另外,当我们用管道连接多个流操作时:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>

流模块中的函数接受任何枚举类型为参数,返回一个流。 流模块还提供了创建流(甚至是无限操作的流)的函数。 例如,Stream.cycle/1可以用来创建一个流,它能无限周期性枚举所提供的参数(小心使用):

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.cycle/1>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

另一方面,Stream.unfold/2函数可以生成给定的有限值:

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]

另一个有趣的函数是Stream.resource/3,它可以用来包裹某资源,确保该资源在使用前打开,在用完后关闭(即使中途出现错误)。–类似C#中的use{}关键字。 比如,我们可以stream一个文件:

iex> stream = File.stream!("path/to/file")
#Function<18.16982430/2 in Stream.resource/3>
iex> Enum.take(stream, 10)

这个例子读取了文件的前10行内容。流在处理大文件,或者慢速资源(如网络)时非常有用。

一开始Enum和流模块中函数的数量多到让人气馁。但你会慢慢地熟悉它们。 建议先熟悉Enum模块,然后因为应用而转去流模块中那些相应的,懒惰版的函数。

11-进程

Elixir中,所有代码都在进程中执行。进程彼此独立,一个接一个并发执行,彼此通过消息传递来沟通。 进程不仅仅是Elixir中最基本的并发单位,它们还是Elixir创建分布式和高容错程序的基础。

Elixir的进程和操作系统中的进程不可混为一谈。 Elixir的进程非常非常地轻量级(在使用CPU和内存角度上说。但是它又不同于其它语言中的线程)。 因此,同时运行着数万个进程也并不是罕见的事。

本章将讲解派生新进程的基本知识,以进程间如何发送和接受消息。

11.1-进程派生

派生(spawning)一个新进程的方法是使用自动导入(kernel函数)的spawn/1函数:

iex> spawn fn -> 1 + 2 end
#PID<0.43.0>

函数spawn/1接收一个_函数_作为参数,在其派生出的进程中执行这个函数。

注意spawn/1返回一个PID(进程标识)。在这个时候,这个派生的进程很可能已经结束。 派生的进程执行完函数后便会结束:

iex> pid = spawn fn -> 1 + 2 end
#PID<0.44.0>
iex> Process.alive?(pid)
false

你可能会得到与例子中不一样的PID

self/0函数获取当前进程的PID:

iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true

而可以发送和接收消息,让进程变得越来越有趣。

11.2-发送和接收

使用send/2函数发送消息,用receive/1接收消息:

iex> send self(), {:hello, "world"}
{:hello, "world"}
iex> receive do
...>   {:hello, msg}  -> msg
...>   {:world, msg} -> "won't match"
...> end
"world"

当有消息被发给某进程,该消息就被存储在该进程的邮箱里。receive/1语句块检查当前进程的邮箱,寻找匹配给定模式的消息。 函数receive/1支持分支子句,如case/2。 当然也可以给子句加上卫兵表达式。

如果找不到匹配的消息,当前进程将一直等待,知道下一条信息到达。但是可以设置一个超时时间:

iex> receive do
...>   {:hello, msg}  -> msg
...> after
...>   1_000 -> "nothing after 1s"
...> end
"nothing after 1s"

超时时间设为0表示你知道当前邮箱内肯定有邮件存在,很自信,因此设了这个极短的超时时间。

把以上概念综合起来,演示进程间发送消息:

iex> parent = self()
#PID<0.41.0>
iex> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.48.0>
iex> receive do
...>   {:hello, pid} -> "Got hello from #{inspect pid}"
...> end
"Got hello from #PID<0.48.0>"

在shell中执行程序时,辅助函数flush/0很有用。它清空缓冲区,打印进程邮箱中的所有消息:

iex> send self(), :hello
:hello
iex> flush()
:hello
:ok

11.3-链接

Elixir中最常用的进程派生方式是通过函数spawn_link/1。 在举例子讲解spawn_link/1之前,来看看如果一个进程失败了会发生什么:

iex> spawn fn -> raise "oops" end
#PID<0.58.0>

。。。啥也没发生。这时因为进程都是互不干扰的。如果我们希望一个进程中发生失败可以被另一个进程知道,我们需要链接它们。 使用spawn_link/1函数,例子:

iex> spawn_link fn -> raise "oops" end
#PID<0.60.0>
** (EXIT from #PID<0.41.0>) an exception was raised:
    ** (RuntimeError) oops
        :erlang.apply/2

当失败发生在shell中,shell会自动终止执行,并显示失败信息。这导致我们没法看清背后过程。 要弄明白链接的进程在失败时发生了什么,我们在一个脚本文件使用spawn_link/1并且执行和观察它:

# spawn.exs
spawn_link fn -> raise "oops" end

receive do
  :hello -> "let's wait until the process fails"
end

这次,该进程在失败时把它的父进程也弄停止了,因为它们是链接的。
手动链接进程:Process.link/1。 建议可以多看看Process模块,里面包含很多常用的进程操作函数。

进程和链接在创建能高容错系统时扮演重要角色。在Elixir程序中,我们经常把进程链接到某“管理者”上。 由这个角色负责检测失败进程,并且创建新进程取代之。因为进程间独立,默认情况下不共享任何东西。 而且当一个进程失败了,也不会影响其它进程。 因此这种形式(进程链接到“管理者”角色)是唯一的实现方法。

其它语言通常需要我们来try-catch异常,而在Elixir中我们对此无所谓,放手任进程挂掉。 因为我们希望“管理者”会以更合适的方式重启系统。 “要死你就快一点”是Elixir软件开发的通用哲学。

在讲下一章之前,让我们来看一个Elixir中常见的创建进程的情形。

11.4-状态

目前为止我们还没有怎么谈到状态。但是,只要你创建程序,就需要状态。 例如,保存程序的配置信息,或者分析一个文件先把它保存在内存里。 你怎么存储状态?

进程就是(最常见的)答案。我们可以写无限循环的进程,保存一个状态,然后通过收发信息来告知或改变该状态。 例如,写一个模块文件,用来创建一个提供k-v仓储服务的进程:

defmodule KV do
  def start do
    {:ok, spawn_link(fn -> loop(%{}) end)}
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end
end

注意start函数简单地派生一个新进程,这个进程以一个空的图为参数,执行loop/1函数。 这个loop/1函数等待消息,并且针对每个消息执行合适的操作。 加入受到一个:get消息,它把消息发回给调用者,然后再次调用自身loop/1,等待新消息。 当受到:put消息,它便用一个新版本的图变量(里面的k-v更新了)再次调用自身。

执行一下试试:

iex> {:ok, pid} = KV.start
#PID<0.62.0>
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
nil

一开始进程内的图变量是没有键值的,所以发送一个:get消息并且刷新当前进程的收件箱,返回nil。 下面再试试发送一个:put消息:

iex> send pid, {:put, :hello, :world}
#PID<0.62.0>
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
:world

注意进程是怎么保持一个状态的:我们通过同该进程收发消息来获取和更新这个状态。 事实上,任何进程只要知道该进程的PID,都能读取和修改状态。

还可以注册这个PID,给它一个名称。这使得人人都知道它的名字,并通过名字来向它发送消息:

iex> Process.register(pid, :kv)
true
iex> send :kv, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
:world

使用进程维护状态,以及注册进程都是Elixir程序非常常用的方式。 但是大多数时间我们不会自己实现,而是使用Elixir提供的抽象实现。 例如,Elixir提供的agent就是一种维护状态的简单的抽象实现:

iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.72.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world

Agent.start/2方法加一个一个:name选项可以自动为其注册一个名字。 除了agents,Elixir还提供了创建通用服务器(generic servers,称作GenServer)、 通用时间管理器以及事件处理器(又称GenEvent)的API。 这些,连同“管理者”树,都可以在Mix和OTP手册里找到详细说明。

12-I/O

本章简单介绍Elixir的输入、输出机制,以及相关的模块,如IO文件路径

现在介绍I/O似乎有点早,但是I/O系统可以让我们一窥Elixir哲学,满足我们对该语言以及VM的好奇心。

12.1-IO模块

IO模块是Elixir语言中读写标准输入、输出、标准错误、文件、设备的主要机制。 使用该模块的方法颇为直接:

iex> IO.puts "hello world"
"hello world"
:ok
iex> IO.gets "yes or no? "
yes or no? yes
"yes\n"

IO模块中的函数默认使用标准输入输出。 我们也可以传递:stderr来指示将错误信息写到标准错误设备上:

iex> IO.puts :stderr, "hello world"
"hello world"
:ok

12.2-文件模块

文件模块包含了可以让我们读写文件的函数。 默认情况下文件是以二进制模式打开,它要求程序员使用特殊的IO.binread/2IO.binwrite/2函数来读写文件:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}
iex> IO.binwrite file, "world"
:ok
iex> File.close file
:ok
iex> File.read "hello"
{:ok, "world"}

文件可以使用:utf8编码打开,然后就可以被IO模块中其他函数使用了:

iex> {:ok, file} = File.open "another", [:write, :utf8]
{:ok, #PID<0.48.0>}

除了打开、读写文件的函数,文件模块还有许多函数来操作文件系统。这些函数根据Unix功能相对应的命令命名。 如File.rm/1用来删除文件;File.mkdir/1用来创建目录;File.mkdir_p/1创建目录并保证其父目录一并创建; 还有File.cp_r/2File.rm_rf/2用来递归地复制和删除整个目录。

你还会注意到文件模块中,函数一般有一个名称类似的版本。区别是名称上一个有!(bang)一个没有。 例如,上面的例子我们在读取“hello”文件时,用的是不带!号的版本。 下面用例子演示下它们的区别:

iex> File.read "hello"
{:ok, "world"}
iex> File.read! "hello"
"world"
iex> File.read "unknown"
{:error, :enoent}
iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory

注意看,当文件不存在时,带!号的版本会报错。就是说不带!号的版本能照顾到模式匹配出来的不同的情况。 但有的时候,你就是希望文件在那儿,!使得它能报出有意义的错误。 因此,不要写:

{:ok, body} = File.read(file)

相反地,应该这么写:

case File.read(file) do
  {:ok, body} -> # handle ok
  {:error, r} -> # handle error
end

或者

File.read!(file)

12.3-路径模块

文件模块中绝大多数函数都以路径作为参数。通常这些路径都是二进制,可以被路径模块提供的函数操作:

iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"


有了以上介绍的几个模块和函数,我们已经能对文件系统进行基本的IO操作。 下面将讨论IO令人好奇的高级话题。这部分不是写Elixir程序必须掌握的,可以跳过不看。 但是如果你大概地看看,可以了解一下IO是如何在VM上实现以及其它一些有趣的内容。

12.4-进程和组长

你可能已经发现,File.open/2函数返回了一个包含PID的元祖:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}

这是因为IO模块实际上是同进程协同工作的。当你调用IO.write(pid, binary)时,IO模块将发送一条消息给执行操作的进程。 让我们用自己的代码表述下这个过程:

iex> pid = spawn fn ->
...>  receive do: (msg -> IO.inspect msg)
...> end
#PID<0.57.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.41.0>, #PID<0.57.0>, {:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated

调用IO.write/2之后,可以看见打印出了发给IO模块的请求。然而因为我们没有提供某些东西,这个请求失败了。

StringIO模块提供了一个基于字符串的IO实现:

iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.43.0>}
iex> IO.read(pid, 2)
"he"

Erlang虚拟机用进程给IO设备建模,允许同一个网络中的不同节点通过交换文件进程,实现节点间的文件读写。 在所有IO设备之中,有一个特殊的进程,称作组长(group leader)。

当你写东西到标准输出,实际上是发送了一条消息给组长,它把内容写给*STDIO文件表述者*:

iex> IO.puts :stdio, "hello"
hello
:ok
iex> IO.puts Process.group_leader, "hello"
hello
:ok

组长可为每个进程做相应配置,用于处理不同的情况。 例如,当在远程终端执行代码时,它能保证远程机器的消息可以被重定向到发起操作的终端上。

12.5-*iodata*和chardata

在以上所有例子中,我们都用的是二进制/字符串方式读写文件。 在“二进制、字符串和字符列表”那章里,我们注意到字符串就是普通的bytes,而字符列表是code points的列表。

IO模块和文件模块中的函数接受列表作为参数。这也就算了,其实还可以接受混合类型的列表,里面是整形、二进制都行:

iex> IO.puts 'hello world'
hello world
:ok
iex> IO.puts ['hello', ?\s, "world"]
hello world
:ok

尽管如此,有些地方还是要注意。一个列表可能表示一串byte,或者一串字符。用哪一种看IO设备是怎么编码的。 如果不指明编码,文件就以raw模式打开,这时候只能用文件模块里bin*开头(二进制版)的函数对其进行操作。 这些函数接受*iodata*作为参数,即,它们期待一个整数值的列表,用来表示byte或二进制。

尽管只是细微的差别,但你只需要考虑那些细节,如果你打算传递列表给那些函数。 底层的bytes已经可以表示二进制,这种表示就是raw的。

13-别名和代码引用

为了实现软件重用,Elixir提供了三种指令(directives)。之所以称之为“指令”是因为它们的作用域是词法作用域(lexicla scope)。

13.1-别名

alias可以为任何模块名设置别名。想象一下Math模块,它针对特殊的数学运算使用了特殊的列表实现:

defmodule Math do
  alias Math.List, as: List
end

现在,任何对List的引用将被自动变成对Math.List的引用。 如果还想访问原来的List,需要前缀’Elixir’:

List.flatten             #=> uses Math.List.flatten
Elixir.List.flatten      #=> uses List.flatten
Elixir.Math.List.flatten #=> uses Math.List.flatten

Elixir中定义的所有模块都在一个主Elixir命名空间。但是为方便起见,我们平时都不再前面加‘Elixir’。

别名常被使用与定义快捷方式中。实际上不带as选项去调用alias会自动将这个别名设置为模块名的最后一部分:

alias Math.List

就相当于:

alias Math.List, as: List

注意,别名是词法作用域。即,允许你在某个函数中设置别名:

defmodule Math do
  def plus(a, b) do
    alias Math.List
    # ...
  end

  def minus(a, b) do
    # ...
  end
end

上面例子中,alias指令只在函数plus/2中有用,minus/2不受影响。

13.2-require

Elixir提供了许多宏,用于元编程(写能生成代码的代码)。

宏就是一堆代码,只是它是在编译时被展开和执行。就是说,为了使用一个宏,你需要确保它的模块和实现在编译时可用。 这使用require指令:

iex> Integer.odd?(3)
** (CompileError) iex:1: you must require Integer before invoking the macro Integer.odd?/1
iex> require Integer
nil
iex> Integer.odd?(3)
true

Elixir中,Integer.odd?/1函数被定义为一个宏,因此他可以被当作卫兵表达式(guards)使用。 为了调用这个宏,你首先得确保用require引用了_Integer_模块。

总的来说,一个模块在被用到之前不需要被require引用,除非我们想让这个宏在整个模块中可用。 尝试调用一个没有引入的宏会导致报错。注意,像alias指令一样,require也是词法作用域的。 下文中我们会进一步讨论宏。

13.3-import

当我们想轻松地访问别的模块中的函数和宏时,我们使用import指令加上那个模块完整的名字。 例如,如果我们想多次使用List模块中的duplicate函数,我们可以简单地import它:

iex> import List, only: [duplicate: 2]
nil
iex> duplicate :ok, 3
[:ok, :ok, :ok]

这个例子中,我们只从List模块导入了函数duplicate/2。尽管only:选项是可选的,但是推荐使用。 除了only:选项,还有except:选项。 使用选项only:还可以传递给它:macros:functions。如下面例子,程序仅导入Integer模块中所有的宏:

import Integer, only: :macros

或者,仅导入所有的函数:

import Integer, only: :functions

注意,import也是词法作用域,意味着我们可以在某特定函数中导入宏或方法:

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    # call duplicate
  end
end

在此例子中,导入的函数List.duplicate/2只在那个函数中可见。这个模块中其它函数都不可用(自然,别的模块也不受影响)。

注意,若import一个模块,将自动require它。

13.4-别名机制

讲到这里你会问,Elixir的别名到底是什么,它是怎么实现的?

Elixir中的别名是以大写字母开头的标识符(像String, Keyword等等),在编译时会被转换为原子。 例如,别名‘String’会被转换为:"Elixir.String"

iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String"
String

使用alias/2指令,其实只是简单地改变了这个别名将要转换的结果。

别名如此工作,是因为在Erlang虚拟机中(以及后来的Elixir),模块是被表述成原子的。例如,我们调用一个Erlang模块的机制是:

iex> :lists.flatten([1,[2],3])
[1, 2, 3]

这也是允许我们动态调用某模块内给定函数的机制:

iex> mod = :lists
:lists
iex> mod.flatten([1,[2],3])
[1,2,3]

一句话,我们只是简单地在原子:lists上调用了函数flatten

13.5-嵌套

介绍了别名,现在可以讲讲嵌套(nesting)以及它在Elixir中是如何工作的。

考虑下面的例子:

defmodule Foo do
  defmodule Bar do
  end
end

该例子定义了两个模块_Foo_和_Foo.Bar_。后者在_Foo_中可以用_Bar_为名来访问,因为它们在同一个词法作用域中。 如果之后开发者决定把Bar模块挪到另一个文件中,那它就需要以全名(Foo.Bar)或者别名来指代。

换句话说,上面的代码等同于:

defmodule Elixir.Foo do
  defmodule Elixir.Foo.Bar do
  end
  alias Elixir.Foo.Bar, as: Bar
end

在以后章节我们可以看到,别名在宏机制中扮演了很重要的角色,来保证宏是干净的(hygienic)。

讨论到这里,模块基本上讲得差不多了。之后会讲解模块的属性。

14-模块属性

在Elixir中,模块属性(attributes)主要服务于三个目的: 1. 作为一个模块的注释,通常附加上用户或虚拟机用到的信息 2. 作为常量 3. 在编译时作为一个临时的模块存储

让我们一个一个讲解。

14.1-作为注释

Elixir从Erlang带来了模块属性的概念。例子:

defmodule MyServer do
  @vsn 2
end

这个例子中,我们显式地为该模块设置了版本这个属性。 属性标识@vsn会被Erlang虚拟机的代码装载机制用到,读取并检查该模块是否在某处被更新了。 如果不注明版本号,会设置为这个模块函数的md5 checksum。

Elixir有个好多系统保留属性。这里只列举一些最常用的: - @moduledoc 为当前模块提供文档 - @doc 为该属性后面的函数或宏提供文档 - @behaviour (注意这个单词是英式拼法)用来注明一个OTP或用户自定义行为 - @before_compile 提供一个每当模块被编译之前执行的钩子。这使得我们可以在模块被编译之前往里面注入函数。

@moduledoc和@doc是很常用的属性,推荐经常使用(写文档)。

Elixir视文档为一等公民,提供了很多方法来访问文档。

让我们回到上几章定义的Math模块,为它添加文档,然后依然保存在math.ex文件中:

defmodule Math do
  @moduledoc """
  Provides math-related functions.

  ### Examples

      iex> Math.sum(1, 2)
      3

  """

  @doc """
  Calculates the sum of two numbers.
  """
  def sum(a, b), do: a + b
end

上面例子使用了heredocs注释。heredocs是多行的文本,用三个引号包裹,保持里面内容的格式。 下面例子演示在iex中,用h命令读取模块的注释:

$ elixirc math.ex
$ iex
iex> h Math # Access the docs for the module Math
...
iex> h Math.sum # Access the docs for the sum function
...

Elixir还提供了ExDoc工具,利用注释文档生成HTNL页。

你可以看看模块里面列出的模块属性列表, 看看Elixir还支持那些模块属性。

Elixir还是用这些属性来定义typespecs: - @spec 为一个函数提供specification - @callback 为行为回调函数提供spec - @type 定义一个@spec中用到的类型 - @typep 定义一个私有类型,用于@spec - @opaque 定义一个opaque类型用于@spec

本节讲了一些内置的属性。当然,属性可以被开发者、或者被类库扩展来支持自定义的行为。

14.2-作为常量

Elixir开发者经常会模块属性当作常量使用:

defmodule MyServer do
  @initial_state %{host: "147.0.0.1", port: 3456}
  IO.inspect @initial_state
end

不同于Erlang,默认情况下用户定义的属性不被存储在模块里。属性值仅在编译时存在。 开发者可以通过调用Module.register_attribute/3来使属性的行为更接近Erlang。

访问一个未定义的属性会报警告:

defmodule MyServer do
  @unknown
end
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it to nil before access

最后,属性也可以在函数中被读取: ``` defmodule MyServer do @my_data 14 def first_data, do: @my_data @my_data 13 def second_data, do: @my_data end

MyServer.first_data #=> 14 MyServer.second_data #=> 13


注意在函数内读取某属性读取的是该属性当前值的快照。换句话说,读取的是编译时的值,而非运行时。
我们即将看到,这点使得属性可以作为模块在编译时的临时存储。

### 14.3-作为临时存储
Elixir组织中有一个项目,叫做[Plug](https://github.com/elixir-lang/plug)。这个项目的目标是创建一个Web的库和框架。

>类似于rack?

Plug库允许开发者定义它们自己的plug,可以在一个web服务器上运行:

defmodule MyPlug do use Plug.Builder

plug :set_header plug :send_ok

def set_header(conn, _opts) do put_resp_header(conn, “x-header”, “set”) end

def send_ok(conn, _opts) do send(conn, 200, “ok”) end end

IO.puts “Running MyPlug with Cowboy on http://localhost:4000" Plug.Adapters.Cowboy.http MyPlug, []


上面例子我们用了```plug/1```宏来连接各个在处理请求时会被调用的函数。
在内部,每当你调用```plug/1```时,Plug库把参数存储在@plug属性里。
在模块被编译之前,Plug执行一个回调函数,这个函数定义了处理http请求的方法。
这个方法将顺序执行所有保存在@plug属性里的plugs。

为了理解底层的代码,我们需要宏。因此我们将回顾一下元编程手册里这种模式。
但是这里的焦点是怎样使用属性来存储数据,让开发者得以创建DSL。

另一个例子来自ExUnit框架,它使用模块属性作为注释和存储:

defmodule MyTest do use ExUnit.Case

@tag :external test “contacts external service” do # … end end


ExUnit中,@tag标签被用来注释该测试用例。之后,这些标签可以作为过滤测试用例之用。
例如,你可以避免执行那些被标记成```:external```的测试,因为它们执行起来很慢。


本章带你一窥Elixir元编程的冰山一角,讲解了模块属性在开发中是如何扮演关键角色的。

下一章将讲解结构体和协议。

## 15-结构体

在之前的几章中,我们谈到过图:

iex> map = %{a: 1, b: 2} %{a: 1, b: 2} iex> map[:a] 1 iex> %{map | a: 3} %{a: 3, b: 2}


结构体是基于图的一个扩展。它将默认值,编译时保证和多态性带入Elixir。

定义一个结构体,你只需在某模块中调用```defstruct/1```:

iex> defmodule User do …> defstruct name: “john”, age: 27 …> end {:module, User, <<70, 79, 82, ...>>, {:struct, 0}}

 
 现在可以用```%User()```语法创建这个结构体的“实例”了:

iex> %User{} %User{name: “john”, age: 27} iex> %User{name: “meg”} %User{name: “meg”, age: 27} iex> is_map(%User{}) true


结构体的编译时保证指的是编译时会检查结构体的字段存不存在:

iex> %User{oops: :field} ** (CompileError) iex:3: unknown key :oops for struct User


当讨论图的时候,我们演示了如何访问和修改图现有的字段。结构体也是一样的:

iex> john = %User{} %User{name: “john”, age: 27} iex> john.name “john” iex> meg = %{john | name: “meg”} %User{name: “meg”, age: 27} iex> %{meg | oops: :field} ** (ArgumentError) argument error


通过使用这种修改的语法,虚拟机知道没有新的键增加到图/结构体中,使得图可以在内存中共享它们的结构。
在上面例子中,john和meg共享了相同的键结构。

结构体也能用在模式匹配中,它们保证结构体有相同的类型:

iex> %User{name: name} = john %User{name: “john”, age: 27} iex> name “john” iex> %User{} = %{} ** (MatchError) no match of right hand side value: %{}


匹配能工作,是由于结构体再底层图中有个字段叫```__struct__```:

iex> john.struct User


总之,结构体就是个光秃秃的图外加一个默认字段。我们这里说的光秃秃的图指的是,为图实现的协议都不能用于结构体。
例如,你不能枚举也不能访问一个结构体:

iex> user = %User{} %User{name: “john”, age: 27} iex> user[:name] ** (Protocol.UndefinedError) protocol Access not implemented for %User{age: 27, name: “john”}


结构体也不是字典,因为也不适用字典模块的函数:

iex> Dict.get(%User{}, :name) ** (ArgumentError) unsupported dict: %User{name: “john”, age: 27}


下一章我们将介绍结构体是如何同协议进行交互的。

## 16-协议


协议是实现Elixir多态性的重要机制。任何数据类型只要实现了某协议,那么该协议的分发就是可用的。
让我们看个例子。

在Elixir中,只有false和nil被认为是false。其它的值都被认为是true。
根据程序需要,有时需要一个```blank?```协议,返回一个布尔值,以说明该参数是否为空。
举例来说,一个空列表或者空二进制可以被认为是空的。

我们可以如下定义协议:

defprotocol Blank do @doc “Returns true if data is considered blank/empty” def blank?(data) end


这个协议期待一个函数```blank?```,它接受一个待实现的参数。
我们为不同的数据类型实现这个协议:

Integers are never blank

defimpl Blank, for: Integer do def blank?(_), do: false end

Just empty list is blank

defimpl Blank, for: List do def blank?([]), do: true def blank?(_), do: false end

Just empty map is blank

defimpl Blank, for: Map do # Keep in mind we could not pattern match on %{} because # it matches on all maps. We can however check if the size # is zero (and size is a fast operation). def blank?(map), do: map_size(map) == 0 end

Just the atoms false and nil are blank

defimpl Blank, for: Atom do def blank?(false), do: true def blank?(nil), do: true def blank?(_), do: false end


我们可以为所有内建数据类型实现协议:
  - 原子
  - BitString
  - 浮点型
  - 函数
  - 整型
  - 列表
  - 图
  - PID
  - Port
  - 引用
  - 元祖

现在手边有了一个定义并被实现的协议,如此使用之:

iex> Blank.blank?(0) false iex> Blank.blank?([]) true iex> Blank.blank?([1, 2, 3]) false


给它传递一个并没有实现该协议的数据类型,会导致报错:

iex> Blank.blank?(“hello”) ** (Protocol.UndefinedError) protocol Blank not implemented for “hello”


### 16.1-协议和结构体
协议和结构体一起使用能够大大加强Elixir的可扩展性。<br/>

在前面几章中我们知道,尽管结构体就是图,但是它们和图并不共享各自协议的实现。
像前几章一样,我们先定义一个名为```User```的结构体:

iex> defmodule User do …> defstruct name: “john”, age: 27 …> end {:module, User, <<70, 79, 82, ...>>, {:struct, 0}}

 然后看看:

iex> Blank.blank?(%{}) true iex> Blank.blank?(%User{}) ** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: “john”}


结构体没有使用协议针对图的实现,而是使用它自己的协议实现:

defimpl Blank, for: User do def blank?(_), do: false end


如果愿意,你可以定义你自己的语法来检查一个user为不为空。
不光如此,你还可以使用结构体创建更强健的数据类型,比如队列,然后实现所有相关的协议,比如枚举(Enumerable)或检查是否为空。

有些时候,程序员们希望给结构体提供某些默认的协议实现。因为显式给所有结构体都实现某些协议实在是太枯燥了。
这引出了下一节“回归大众”(falling back to any)的说法。

### 16.2-回归大众
能够给所有类型提供默认的协议实现肯定是很方便的。在定义协议时,把```@fallback_to_any```设置为```true```即可:

defprotocol Blank do @fallback_to_any true def blank?(data) end

现在这个协议可以被这么实现:

defimpl Blank, for: Any do def blank?(_), do: false end


现在,那些我们还没有实现```Blank```协议的数据类型(包括结构体)也可以来判断是否为空了。

### 16.3-内建协议
Elixir自带了一些内建协议。在前面几章中我们讨论过枚举模块,它提供了许多方法。
只要任何一种数据结构它实现了Enumerable协议,就能使用这些方法:

iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end [2,4,6] iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end 6


另一个例子是```String.Chars```协议,它规定了如何将包含字符的数据结构转换为字符串类型。
它暴露为函数```to_string```:

iex> to_string :hello “hello”


注意,在Elixir中,字符串插值操作调用的是```to_string```函数:

iex> “age: #{25}” “age: 25”

上面代码能工作是因为数字类型实现了```String.Chars```协议。如果传进去的是元组就会报错:

iex> tuple = {1, 2, 3} {1, 2, 3} iex> “tuple: #{tuple}” ** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}


当想要打印一个比较复杂的数据结构时,可以使用```inspect```函数。该函数基于协议```Inspect```:

iex> “tuple: #{inspect tuple}” “tuple: {1, 2, 3}”


_Inspect_协议用来将任意数据类型转换为可读的文字表述。IEx用来打印表达式结果用的就是它:

iex> {1, 2, 3} {1,2,3} iex> %User{} %User{name: “john”, age: 27}


记住,习惯上来说,无论何时,头顶#号被插的值,会被表现成一个不合语法的字符串。
在转换为可读的字符串时丢失了信息,因此别指望还能从该字符串取回原来的那个对象:

iex> inspect &(&1+2) “#Function<6.71889879/1 in :erl_eval.expr/5>“ ```

Elixir中还有些其它协议,但本章就讲这几个比较常用的。下一章将讲讲Elixir中的错误捕捉以及异常。

jack.zh 标签:Elixir 继续阅读

3467 ℉

15.10.07

Elixir入门教程<上>

1. 简介

1.1 安装

  • Windows平台,Elixir提供了exe安装程序,自己去下载
  • Windows Mac OS 和Linux 请先安装Erlang17+,再下载Elixir make

1.2 交互模式

运行iex

1.3 执行脚本

elixir simple.exs

2. 基本数据类型

本章介绍Elixir一些基本的类型, 如:整型(integer),浮点型(float),布尔(boolean),原子(atom,又称symbol),字符串(string),列表(list)和元组(tuple)等。 它们在iex中显示如下:

iex> 1          # integer
iex> 0x1F       # integer
iex> 1.0        # float
iex> true       # boolean
iex> :atom      # atom / symbol
iex> "elixir"   # string
iex> [1, 2, 3]  # list
iex> {1, 2, 3}  # tuple

2.1 基本算数运算

打开iex,输入以下表达式:

iex> 1 + 2
3
iex> 5 * 5
25
iex> 10 / 2
5.0

10 / 2返回了一个浮点型的5.0而非整型的5,这是预期的。在Elixir中,/运算符总是返回浮点型数值。

如果你想进行整型除法,或者求余数,可以使用函数divrem。(rem的意思是division remainder):

iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

在写函数参数时,括号是可选的。(ruby程序员会心一笑)

Elixir支持用捷径(shortcut)书写二进制、八进制、十六进制整数,如:

iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31

揉揉眼,八进制是0o,数字0+小写o。

输入浮点型数字需要一个小数点,且在其后至少有一位数字。Elixir支持使用e来表示指数:

iex> 1.0
1.0
iex> 1.0e-10
1.0e-10

Elixir中浮点型都是64位双精度。

2.2 布尔

Elixir使用truefalse两个布尔值。

iex> true
true
iex> true == false
false

Elixir提供了许多用以判断类型的函数,如is_boolean/1函数可以用来检查参数是不是布尔型。

在Elixir中,函数通过名称和参数个数(又称元数,arity)来识别。 如is_boolean/1表示名为is_boolean,接受一个参数的函数; 而is_boolean/2表示与其同名、但接受2个参数的不同函数。(只是打个比方,这样的is_boolean实际上不存在)

另外,<函数名>/<元数>这样的表述为了在讲述函数时方便。在实际程序中如果调用函数, 是不用注明/1或是/2的。

iex> is_boolean(true)
true
iex> is_boolean(1)
false

类似的函数还有is_integer/1is_float/1is_number/1,分别测试参数是否是整型、浮点型或者两者其一。

可以在交互式命令行中使用h命令来打印函数或运算符的帮助信息。如h is_boolean/1h ==/2。注意此处提及某个函数时,不但要给出名称,还要加上元数/<arity>

2.3 原子

原子(atom)是一种常量,变量名就是它的值。有些语言(如ruby)中称其为符号(symbol):

iex> :hello
:hello
iex> :hello == :world
false

布尔值truefalse实际上就是原子:

iex> true == :true
true
iex> is_atom(false)
true  

此外原子也支持:"原子名"的方式 大写字母开头的”变量”实际是:"Elixir.变量名"的别名:

iex> Hello == :"Elixir.Hello"
true  

但如果这个别名已经有Elixir.前缀了 那么就不会再有Elixir前缀了:

iex> Elixir.Hello == :"Elixir.Hello"
true

模块调用其实也是使用到了这种原子别名:

iex> IO == :"Elixir.IO"
true
iex> :"Elixir.IO".puts "an atom"
an atom

2.4 字符串

在Elixir中,字符串以双括号包裹,采用UTF-8编码:

iex> "hellö"
"hellö"

Elixir支持字符串插值(语法类似ruby):

iex> "hellö #{:world}"
"hellö world"

字符串可以直接包含换行符,或者其转义字符:

iex> "hello
...> world"
"hello\nworld"
iex> "hello\nworld"
"hello\nworld"

你可以使用IO模块(module)里的IO.puts/1方法打印字符串:

iex> IO.puts "hello\nworld"
hello
world
:ok

函数IO.puts/1打印完字符串后,返回原子值:ok

字符串在Elixir内部被表示为二进制数值(binaries),也就是一连串的字节(bytes):

iex> is_binary("hellö")
true

注意,二进制数值(binary)是Elixir内部的存储结构之一。字符串、列表等类型在语言内部就存储为二进制数值,因此它们也可以被专门操作二进制数值的函数修改。

你可以查看字符串包含的字节数量:

iex> byte_size("hellö")
6

为啥是6?不是5个字符么?注意里面有一个非ASCII字符ö,在UTF-8下被编码为2个字节。

我们可以使用专门的函数来返回字符串中的字符数量:

iex> String.length("hellö")
5

String模块中提供了很多符合Unicode标准的函数来操作字符串。 如:

iex> String.upcase("hellö")
"HELLÖ"

记住,单引号和双引号包裹的字符串在Elixir中是两种不同的数据类型:

iex> 'hellö' == "hellö"
false

我们将在之后关于“二进制、字符串与字符列表”章节中详细讲述它们的区别。

2.5 匿名函数

在Elixir中,使用关键字fnend来界定函数。如:

iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> is_function(add)
true
iex> is_function(add, 2)
true
iex> is_function(add, 1)
false
iex> add.(1, 2)
3

在Elixir中,函数是头等公民。你可以将函数作为参数传递给其他函数,就像整型和浮点型一样。 在上面的例子中,我们向函数is_function/1传递了由变量add表示的匿名函数,结果返回true。 我们还可以调用函数is_function/2来判断该参数函数的元数(参数个数)。

注意,在调用一个匿名函数时,在变量名和写参数的括号之间要有个点号(.)。

匿名函数是闭包,意味着它们可以访问当前作用域(scope)内的其它变量:

iex> add_two = fn a -> add.(a, 2) end
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> add_two.(2)
4

这个例子定义的匿名函数add_two它内部使用了之前在同一个iex内定义好的add变量。 但要注意,在匿名函数内修改了所引用的外部变量的值,并不实际反映到该变量上:

iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42

这个例子中匿名函数把引用了外部变量x,并修改它的值为0.这时函数执行后,外部的x没有被影响。

2.6(链式)列表

Elixir使用方括号标识列表。列表可以包含任意类型的值:

iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3

两个列表可以使用++/2拼接,使用--/2做“减法”:

iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]

本教程将多次涉及列表的头(head)和尾(tail)的概念。 列表的头指的是第一个元素,而尾指的是除了第一个元素以外,其它元素组成的列表。 它们分别可以用函数hd/1tl/1从原列表中取出:

iex> list = [1,2,3]
iex> hd(list)
1
iex> tl(list)
[2, 3]

尝试从一个空列表中取出头或尾将会报错:

iex> hd []
** (ArgumentError) argument error

2.7 元组

Elixir使用大括号(花括号)定义元组(tuples)。 类似列表,元组也可以承载任意类型的数据:

iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2

元组使用连续的内存空间存储数据。这意味着可以很方便地使用索引访问元组数据,以及获取元组大小。(索引从0开始):

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2

也可以很方便地使用函数put_elem/3设置某个位置的元素值:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}

注意函数put_elem/3返回一个新元组。原来那个由变量tuple标识的元组没有被改变。 这是因为Elixir的数据类型是不可变的。 这种不可变性使你永远不用担心你的数据会在某处被某些代码改变。 在处理并发程序时,该不可变性有利于减少多个不同程序实体在同时修改一个数据结构时引起的竞争以及其他麻烦。

2.8 列表还是元组?

列表与元组的区别:列表在内存中是以链表的形式存储的,一个元素指向下一个元素,然后再下一个…直到到达列表末尾。我们称这样的一对(元素值+指向下一个元素的指针)为列表的一个单元(cons cell)。 用Elixir语法表示这种模式:

iex> list = [1|[2|[3|[]]]]
[1, 2, 3]

列表方括号中的竖线(|)表示列表头与尾的分界。

这个原理意味着获取列表的长度是一个线性操作:我们必须遍历完整个列表才能知道它的长度。 但是列表的前置拼接操作很快捷:

iex> [0] ++ list
[0, 1, 2, 3]
iex> list ++ [4]
[1, 2, 3, 4]

上面例子中第一条语句是前置拼接操作,执行起来很快。因为它只是简单地添加了一个新列表单元,它指向原先列表头部。而原先的列表没有任何变化。

第二条语句是后缀拼接操作,执行速度较慢。这是因为它重建了原先的列表,让原先列表的末尾元素指向那个新元素。

而另一方面,元组在内存中是连续存储的。这意味着获取元组大小,或者使用索引访问元组元素的操作十分快速。 但是元组在修改或添加元素时开销很大,因为这些操作会在内存中对元组的进行整体复制。

这些讨论告诉我们当如何在不同的情况下选择使用不同的数据结构。

函数常用元组来返回多个信息。如File.read/1,它读取文件内容,返回一个元组:

iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}

如果传递给函数File.read/1的文件路径有效,那么函数返回一个元组,其首元素是原子:ok,第二个元素是文件内容。 如果路径无效,函数也将返回一个元组,其首元素是原子:error,第二个元素是错误信息。

大多数情况下,Elixir会引导你做正确的事。 如有个叫elem/2的函数,它使用索引来访问一个元组元素。 这个函数没有相应的列表版本,因为根据存储机制,列表不适用通过索引来访问:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"

当需要计算某数据结构包含的元素个数时,Elixir遵循一个简单的规则:** 如果操作在常数时间内完成(答案是提前算好的),这样的函数通常被命名为**size***。 而如果操作需要显式计数,那么该函数通常命名为**length*

例如,目前讲到过的4个计数函数:byte_size/1(用来计算字符串有多少字节) ,tuple_size/1(用来计算元组大小) ,length/1(计算列表长度)以及String.length/1(计算字符串中的字符数)。

按照命名规则,当我们用byte_size获取字符串所占字节数时,开销较小。 但是当我们用String.length获取字符串unicode字符个数时,需要遍历整个字符串,开销较大。

除了本章介绍的数据类型,Elixir还提供了PortReferencePID三个数据类型(它们常用于进程交互)。 这些数据类型将在讲解进程时详细介绍。

3. 基本运算符

通过前几章我们知道Elixir提供了 +-*/4个算术运算符,外加整数除法函数div/2和取余函数rem/2。Elixir还提供了++--运算符来操作列表:

iex> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]
iex> [1,2,3] -- [2]
[1,3]

使用<>进行字符串拼接:

iex> "foo" <> "bar"
"foobar"

Elixir还提供了三个布尔运算符:orandnot。这三个运算符只接受布尔值作为第一个参数:

iex> true and true
true
iex> false or is_atom(:example)
true

如果提供了非布尔值作为第一个参数,会报异常:

iex> 1 and true
** (ArgumentError) argument error

orand可短路,即它们仅在第一个参数无法决定整体结果的情况下才执行第二个参数:

iex> false and error("This error will never be raised")
false
iex> true or error("This error will never be raised")
true

如果你是Erlang程序员,Elixir中的andor其实就是andalsoorelse运算符。

除了这几个布尔运算符,Elixir还提供||&&!运算符。它们可以接受任意类型的参数值。 在使用这些运算符时,除了falsenil的值都被视作true

# or
iex> 1 || true
1
iex> false || 11
11

# and
iex> nil && 13
nil
iex> true && 17
17

# !
iex> !true
false
iex> !1
false
iex> !nil
true

根据经验,当参数返回的是布尔时,使用andornot;如果非布尔值,用&&||!

Elixir还提供了==!====!==<=>=<>这些比较运算符:

iex> 1 == 1
true
iex> 1 != 2
true
iex> 1 < 2
true

=====的不同之处是后者在判断数字时更严格:

iex> 1 == 1.0
true
iex> 1 === 1.0
false

在Elixir中,可以判断不同类型数据的大小:

iex> 1 < :atom
true

这很实用。排序算法不必担心如何处理不同类型的数据。总体上,不同类型的排序顺序是:

number < atom < reference < functions < port < pid < tuple < maps < list < bitstring

不用背,只要知道有这么回事儿就可以。

4.模式匹配

  • 匹配运算符
  • 模式匹配
  • pin运算符

本章起教程进入不那么基础的阶段,开始涉及函数式编程概念。 在Elixir中,=运算符实际上是一个匹配运算符。 本章将讲解如何使用=运算符来对数据结构进行模式匹配。最后本章还会讲解pin运算符^,用来访问某变量之前绑定的值。

4.1 匹配运算符

我们已经多次使用=符号进行变量的赋值操作:

iex> x = 1
1
iex> x
1

在Elixir中,=其实称为匹配运算符。下面来学习这样的概念:

iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1

注意1 = x是一个合法的表达式。由于前面给x赋值为1,左右相同,所以它匹配成功了。而两侧不匹配的时候,MatchError将被抛出。

变量只有在匹配操作符=的左侧时才被赋值:

iex> 1 = unknown
** (RuntimeError) undefined function: unknown/0

错误原因是unknown变量没有被赋过值,Elixir猜你想调用一个名叫unknown/0的函数,但是找不到这样的函数。

4.2 模式匹配

匹配运算符不光可以匹配简单数值,还能用来解构复杂的数据类型。 例如,我们在元组上使用模式匹配:

iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"

在两端不匹配的情况下,模式匹配会失败。比方说,匹配两端的元组不一样长:

iex> {a, b, c} = {:hello, "world"}
** (MatchError) no match of right hand side value: {:hello, "world"}

或者两端不是一个类型:

iex> {a, b, c} = [:hello, "world", "!"]
** (MatchError) no match of right hand side value: [:hello, "world", "!"]

有趣的是,我们可以匹配特定值。下面例子中匹配的左端当且仅当右端是个元组,且第一个元素是原子:ok

iex> {:ok, result} = {:ok, 13}
{:ok, 13}
iex> result
13

iex> {:ok, result} = {:error, :oops}
** (MatchError) no match of right hand side value: {:error, :oops}

用在列表上:

iex> [a, b, c] = [1, 2, 3]
[1, 2, 3]
iex> a
1

列表支持匹配自己的headtail(这相当于同时调用hd/1tl/1,给headtail赋值):

iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]

hd/1tl/1函数一样,以上代码不能对空列表使用:

iex> [h|t] = []
** (MatchError) no match of right hand side value: []

[head|tail]这种形式不光在模式匹配时可以用,还可以用作前置数值:

iex> list = [1, 2, 3]
[1, 2, 3]
iex> [0|list]
[0, 1, 2, 3]

模式匹配使得程序员可以容易地解构数据结构(如元组和列表)。在后面我们还会看到,它是Elixir的一个基础,对其它数据结构同样适用,比如图和二进制。

4.3 pin运算符

在Elixir中,变量可以被重新绑定:

iex> x = 1
1
iex> x = 2
2

如果你不想这样,可以使用pin运算符(^)。加上了pin运算符的变量,在匹配时使用的值是匹配前就赋予的值:

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2
iex> {x, ^x} = {2, 1}
{2, 1}
iex> x
2

注意如果一个变量在匹配中被引用超过一次,所有的引用都应该绑定同一个模式:

iex> {x, x} = {1, 1}
1
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}

有些时候,你并不在意模式里的一些值。通常你就可以把它们绑定到特殊的变量“_”上。例如,如果你只想要某列表的head,而不要tail值。你可以这么做:

iex> [h | _] = [1, 2, 3]
[1, 2, 3]
iex> h
1

变量“_”特殊之处在于它不能被读。尝试读取它会报“未绑定的变量”错误:

iex> _
** (CompileError) iex:1: unbound variable _

尽管模式匹配可以让我们创建功能强大的结构,但是它的作用被限制了。 比如,你不能让函数调用作为匹配的左端。下面例子就是非法的:

iex> length([1,[2],3]) = 3
** (CompileError) iex:1: illegal pattern

5. 控制语句

  • case
  • 子句与卫兵表达式
  • cond
  • if和unless
  • do语句块

本章讲解Elixir的流程控制结构。

5.1 case

case将一个值与许多模式进行比较,直到找到一个匹配的:

iex> case {1, 2, 3} do
...>   {4, 5, 6} ->
...>     "This clause won't match"
...>   {1, x, 3} ->
...>     "This clause will match and bind x to 2 in this clause"
...>   _ ->
...>     "This clause would match any value"
...> end

如果与一个已赋值的变量做比较,要用pin运算符(^)标记该变量:

iex> x = 1
1
iex> case 10 do
...>   ^x -> "Won't match"
...>   _  -> "Will match"
...> end

可以加上卫兵表达式提供额外的条件:

iex> case {1, 2, 3} do
...>   {1, x, 3} when x > 0 ->
...>     "Will match"
...>   _ ->
...>     "Won't match"
...> end

上面例子中,第一个待比较的模式多了一个条件:x必须是正数。

5.2 子句中的卫兵表达式

Erlang中只允许以下卫兵表达式出现在子句中:

  • 比较运算符(==,!=,===,!==,>,<,<=,>=)
  • 布尔运算符(and,or)以及否定运算符(not,!)
  • 算数运算符(+,-,*,/)
  • <>和++如果左端是字面值
  • in运算符
  • 以下类型判断函数:
    • is_atom/1
    • is_binary/1
    • is_bitstring/1
    • is_boolean/1
    • is_float/1
    • is_function/1
    • is_function/2
    • is_integer/1
    • is_list/1
    • is_map/1
    • is_number/1
    • is_pid/1
    • is_reference/1
    • is_tuple/1
  • 外加以下函数:
    • abs(number)
    • bit_size(bitstring)
    • byte_size(bitstring)
    • div(integer, integer)
    • elem(tuple, n)
    • hd(list)
    • length(list)
    • map_size(map)
    • node()
    • node(pid | ref | port)
    • rem(integer, integer)
    • round(number)
    • self()
    • tl(list)
    • trunc(number)
    • tuple_size(tuple)

记住,卫兵表达式中出现的错误不会抛出,只会简单地让卫兵条件失败:

iex> hd(1)
** (ArgumentError) argument error
    :erlang.hd(1)
iex> case 1 do
...>   x when hd(x) -> "Won't match"
...>   x -> "Got: #{x}"
...> end
"Got 1"

如果case中没有一条模式能匹配,会报错:

iex> case :ok do
...>   :error -> "Won't match"
...> end
** (CaseClauseError) no case clause matching: :ok

匿名函数也可以像下面这样,用多个模式或卫兵条件来灵活地匹配该函数的参数:

iex> f = fn
...>   x, y when x > 0 -> x + y
...>   x, y -> x * y
...> end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> f.(1, 3)
4
iex> f.(-1, 3)
-3

需要注意的是,所有case模式中表示的参数个数必须一致,否则会报错。

上面的例子两个待匹配模式都是xy。如果再有一个模式表示的参数是xyz,那就不行:

iex(5)> f2 = fn
...(5)>   x,y -> x+y
...(5)>   x,y,z -> x+y+z
...(5)> end
** (CompileError) iex:5: cannot mix clauses with different arities in function definition
    (elixir) src/elixir_translator.erl:17: :elixir_translator.translate/2

5.3 cond

case是拿一个值去同多个值或模式进行匹配,匹配了就执行那个分支的语句。

然而,许多情况下我们要检查不同的条件,找到第一个结果为true的,执行它的分支。 这时我们用cond:

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

这样的写法和命令式语言里的else if差不多一个意思(尽管很少这么写)。

如果没有一个条件结果为true,会报错。因此,实际应用中通常会使用true作为最后一个条件。 因为即使上面的条件没有一个是true,那么该cond表达式至少还可以执行这最后一个分支:

iex> cond do
...>   2 + 2 == 5 ->
...>     "This is never true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   true ->
...>     "This is always true (equivalent to else)"
...> end

用法就好像许多语言中switch语句中的default一样。

最后需要注意的是,cond视所有除了falsenil的数值都为true

iex> cond do
...>   hd([1,2,3]) ->
...>     "1 is considered as true"
...> end
"1 is considered as true"

5.4 if和unless

除了casecond,Elixir还提供了两很常用的宏:if/2unless/2,用它们检查单个条件:

iex> if true do
...>   "This works!"
...> end
"This works!"
iex> unless true do
...>   "This will never be seen"
...> end
nil

如果给if/2的条件结果为false或者nil,那么它在do/end间的语句块就不会执行,该表达式返回nilunless/2相反。

它们都支持else语句块:

iex> if nil do
...>   "This won't be seen"
...> else
...>   "This will"
...> end
"This will"

有趣的是,if/2unless/2是以宏的形式提供的,而不像在很多语言中那样是语句。 可以阅读文档或if/2的源码(Kernel模块)。Kernel模块还定义了诸如+/2运算符和is_function/2函数。它们是默认是被自动导入,因而一开始就在你的代码中可用。

5.5 do语句块

以上讲解的4种流程控制结构:casecondifunless,它们都被包裹在do/end语句块中。 即使我们把if语句写成这样:

iex> if true, do: 1 + 2
3

在Elixir中,do/end语句块方便地将一组表达式传递给do:。以下是等同的:

iex> if true do
...>   a = 1 + 2
...>   a + 10
...> end
13
iex> if true, do: (
...>   a = 1 + 2
...>   a + 10
...> )
13

我们称第二种语法使用了键值列表(keyword lists)。我们可以这样传递else

iex> if false, do: :this, else: :that
:that

注意一点,do/end语句块永远是被绑定在最外层的函数调用上。例如:

iex> is_number if true do
...>  1 + 2
...> end

将被解析为:

iex> is_number(if true) do
...>  1 + 2
...> end

这使得Elixir认为你是要调用函数is_number/2(第一个参数是if true,第二个是语句块)。 这时就需要加上括号解决二义性:

iex> is_number(if true do
...>  1 + 2
...> end)
true

关键字列表在Elixir语言中占有重要地位,在许多函数和宏中都有使用。后文中还会对其进行详解。

6. 二进制数据-字符串-字符列表

  • UTF-8和Unicode
  • 二进制(和bitstring)
  • 字符列表

在“基本类型”一章中介绍了字符串,以及如何使用is_binary/1函数来检查它:

iex> string = "hello"
"hello"
iex> is_binary string
true

本章将学习理解,二进制数据(binaries)是个啥,它怎么和字符串扯上关系的,以及用单引号包裹的值,’像这样’,是啥意思。

6.1 UTF-8和Unicode

Elixir中,字符串就是UTF-8编码的二进制数据(binaries)。为了弄清这句话啥意思,我们要先理解两个概念:bytescode point的区别。

“二进制数据”(binaries)这个翻译不一定准确。它其实就是以二进制为内容的列表。下文有些地方可能会简单写成“二进制”。如有模糊的地方,一般也会用原文标出。

bytescode points就不怎么翻译了(bytes有时会翻译成字节),可根据上下文语境理解。

字母acode point97,而字母łcode point322

当把字符串"hełło"写到硬盘上的时候,需要将其code point转化为字节(bytes)。 如果一个字节对应一个code point,那是存储不了"hełło"的,因为字母ł的code point322,超过了一个字节所能存储的最大数值(255)。

但是如你所见,该字母能够显示到屏幕上,说明还是有一定的解决方法的。方法就是编码。

要用字节表示code point,我们需要在一定程度上对其进行编码。 Elixir使用UTF-8为默认编码格式。 当我们说,某个字符串是UTF-8编码的二进制数据(binaries),这句话意思是该字符串是一串字节,以一定方法组织,来表示特定的一串code points。

因此当我们存储字母ł的时候,实际上是用两个字节来表示它。 这就是为什么有时候对同一字符串调用函数byte_size/1String.length/1结果不一样:

iex> string = "hełło"
"hełło"
iex> byte_size string
7
iex> String.length string
5

UTF-8需要1个字节来表示code points:‘h’‘e’‘o’各用一个,而‘ł’用2个字节。 在Elixir中,可以使用?运算符获取某字符的code point值:

iex> ?a
97
iex> ?ł
322

你还可以使用String模块里的函数,将字符串切成单独的code points:

iex> String.codepoints("hełło")
["h", "e", "ł", "ł", "o"]

Elixir为字符串操作提供了强大的支持。实际上,Elixir通过了文章“字符串类型破了”记录的所有测试。

不仅如此,因为字符串是二进制数据,Elixir还提供了更强大的底层类型的操作。下面就来介绍该底层类型—二进制数据。

6.2 二进制数据(和bitstring)

在Elixir中可以用<<>>定义一个二进制数据:

iex> <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> byte_size <<0, 1, 2, 3>>
4

一个二进制只是一连串字节。这些字节可以以任何方法组织,即使凑不成一个合法的字符串:

iex> String.valid?(<<239, 191, 191>>)
false

字符串的拼接操作实际上是二进制的拼接操作:

iex> <<0, 1>> <> <<2, 3>>
<<0, 1, 2, 3>>

一个常见技巧是,通过给某字符串尾部拼接一个空字节<<0>>,来看看该字符串内部二进制的样子:

iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>

二进制中的每个数值都表示一个byte,因此其最大是255。 如果超出了255,二进制允许你再提供一个修饰符,标识一下那个位置的存储空间大小,使其可以满足存储要求。或者使用修饰符将其转换为utf8编码后的形式(变成多个字节的二进制),再存储:

iex> <<255>>
<<255>>
iex> <<256>> # truncated
<<0>>
iex> <<256 :: size(16)>> # use 16 bits (2 bytes) to store the number
<<1, 0>>
iex> <<256 :: utf8>> # the number is a code point
"Ā"
iex> <<256 :: utf8, 0>>
<<196, 128, 0>>

如果一个byte8 bits,那如果我们给一个size1 bit的修饰符会怎样?:

iex> <<1 :: size(1)>>
<<1::size(1)>>
iex> <<2 :: size(1)>> # truncated
<<0::size(1)>>
iex> is_binary(<< 1 :: size(1)>>)
false
iex> is_bitstring(<< 1 :: size(1)>>)
true
iex> bit_size(<< 1 :: size(1)>>)
1

这样(每个元素是1 bit)就不再是二进制数据(人家每个元素是byte,至少8 bits)了,而是bitstring,就是一串比特! 所以实际上二进制数据(binary)就是一串比特(bitstring),只是它容纳的比特总数必须是8的倍数。

我们也可以对二进制数据或bitstring做模式匹配:

iex> <<0, 1, x>> = <<0, 1, 2>>
<<0, 1, 2>>
iex> x
2
iex> <<0, 1, x>> = <<0, 1, 2, 3>>
** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>

注意(没有修改器标识的情况下)二进制数据中的每个元素都应该匹配8 bits。 因此上面最后的例子,匹配的左右两端不具有相同容量,因此出现错误。

下面是使用了修饰符标识的匹配例子:

iex> <<0, 1, x :: binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> x
<<2, 3>>

上面的模式仅在二进制尾部元素被修改器标识为又一个二进制时才正确。 字符串的连接操作也是一个意思:

iex> "he" <> rest = "hello"
"hello"
iex> rest
"llo"

总之,记住字符串是UTF-8编码的二进制数据,而二进制数据是特殊的、bit数量是8的倍数的bitstring。 这种机制增加了Elixir在处理bitsbytes时的灵活性。 现实中99%的时候你会用到is_binary/1byte_size/1函数,来跟二进制数据打交道。

6.3 字符列表

字符列表就是字符的列表。 双引号包裹字符串,单引号包裹字符列表。

iex> 'hełło'
[104, 101, 322, 322, 111]
iex> is_list 'hełło'
true
iex> 'hello'
'hello'

字符列表存储的不是字节,而是字符的code points(实际上就是这些code points的普通列表)。 如果某字符不属于ASCII范围,iex就打印它的code point。

实际应用中,字符列表常被用来做一些老的库,或者同Erlang平台交互时使用的参数。因为这些老库不接受二进制数据作为参数。

将字符列表和字符串之间转换,使用函数to_string/1to_char_list/1

iex> to_char_list "hełło"
[104, 101, 322, 322, 111]
iex> to_string 'hełło'
"hełło"
iex> to_string :hello
"hello"
iex> to_string 1
"1"

注意这些函数是多态的。它们不但转化字符列表和字符串,还能转化字符串和整数,等等。

jack.zh 标签:Elixir 继续阅读

1 2 3 4 5 6 7 8 9 10
Fork me on GitHub