Elixir in Action-Manning 2015(读书笔记)
Elixir in Action
目录
语言
First steps
- Erlang/BEAM并不是最快的,但它的设计目标是支持大并发和可预测的性能
Building blocks
-
It’s time to start learning about Elixir.(哈哈)
-
模块名中的.仅仅上逻辑上的管理方便,不代表模块之间的嵌套包含关系
-
defmodule、def不是关键字,而是宏
-
管道:-5 |> abs |> Integer.to_string |> IO.puts(对应后面函数的第一个参数 )
-
参数默认值:x \ 0 (这个语法略显怪异,不过PHP里用\作为名字空间分隔符也蛮诡异的)
-
函数用参数个数区分,=> 没有可变参数 (C/C++ varargs ...)
-
import IO:可以直接写puts,而不需要IO.puts全称
-
alias IO, as: MyIO
-
模块属性(常量):@pi 3.1415926 (这些不同语言语法的细微之处真的很繁琐的)
-
类型规范:@spec area(number) :: number(Erlang没有静态类型系统??)
1. @spec insert_at(list, integer, any) :: list -
注释
-
:an_atom(可以去掉:,改写为AnAtom)
-
nil || false || 5 || true (返回5,这里的特性跟JS一样)
-
Tuples
1. person = {"Bob", 25}
2. age = elem(person, 1) -
Lists
1. Enum.at(prime_numbers, 4)
2. List.replace_at(prime_numbers, 0, 11) -
Immutability(修改元素属性返回一个新元素)
1. Elixir isn’t a pure functional language,(文件IO操作),但是变量不会被修改(只有‘重新bind’) -
Maps
1. bob = %{:name => "Bob", :age => 25, :works_at => "Initech"}
2. next_years_bob = %{bob | age: 26} #必须是已经存在的values
3. Map.put(bob, :salary, 50000) ==> Dict.put(bob, :salary, 50000) -
Binaries and bitstrings
1. <<1, 2, 3>> 3字节数据
2. 指定位宽度:<<1::4, 15::4>>
3. <<1, 2>> <> <<3, 4>> 拼接2个比特串 -
Strings
1. ~S(Not escaped \n) ~S(Not interpolated #{3 + 0.14})
2. Heredoc:""" ...... """ -
First-class functions
1. square = fn(x) -> x*x end
2. square.(3) #匿名函数的调用加.
3. 捕获操作:&IO.puts/1
lambda = &(&1 * &2 + &3) -
Other built-in types
1. 引用:Kernel.make_ref/0 (or make_ref ). 2^82
2. pid
3. port -
Higher-level types
1. range = 1..2
2. 关键词列表:days = [monday: 1, tuesday: 2, wednesday: 3] # 枚举?
Keyword.get(days, :monday)
用途:opts \ []
3. HashDict
-
IO lists(一颗深度嵌套的树,其中叶子节点是bytes数据)
1. iolist = [[['H', 'e'], "llo,"], " worl", "d!"] #输出到IO设备时会被flatten? -
Operators:主要就是一个 |>
1. +实际上是函数,Kernel.+ -
Macros
-
Special forms:&(...) for receive try
-
理解运行时:
1. elixirc source.ex 2. iex(1)> apply(IO, :puts, ["Dynamic function call."]) #三元组MFA 3. elixir my_source.exs #解释执行???
Control flow
- 理解模式匹配
1. {name, age} = {"Bob", 25}
2. person = {:person, "Bob", 25}
3. {:ok, contents} = File.read("my_app.config") #注意这里变量是在{}内被初始化的,它的作用域到哪里?
4. ^ pin操作(预先指定变量的位置):
expected_name = "Bob"
{^expected_name, _} = {"Bob", 25}
5. map的部分匹配:%{age: age} = %{name: "Bob", age: 25}
6. 二进制: <<b1, rest :: binary>> = binary
<<a :: 4, b :: 4>> = <<155>> # 这个地方比较神奇,可用于解析二进制文件/网络流
7. "ping " <> url = command (这个地方怎么不用正则表达式的)
8. 函数
参数个数相同,但形式上不同的函数只是同一个函数的不同子句(clause)
Guards:def ... when ... do ... end
类型顺序:number < atom < reference < fun < port < pid < tuple < map < list < bitstring (binary)
Multiclause lambdas:fn pattern_1 -> ... pattern_2 -> ... ... end
- Conditionals
1. 可以直接把条件作为约束写到函数形参定义里:
defp lines_num({:ok, contents}) do contents |> String.split("\n") |> length end
2. if condition, do: something, else: another_thing
3. cond do
4. case expression do
-
循环和迭代
1. 数据不可变 => 没有for/while循环结构 ==> 使用尾递归 :accumulator -
高阶函数
1. Enum.reduce/3 -
Comprehensions
1. for x <- [1, 2, 3], y <- [1, 2, 3], (这里*)do: {x, y, x*y} #语法有点像Scala(多重循环只有一个for)
- into: %{}
-
Streams(lazy数据结构)
1. stream = [1, 2, 3] |> Stream.map(fn(x) -> 2 * x end)
2. File.stream!(path)|> Stream.map(&String.replace(&1, "\n", ""))
|> Enum.filter(&(String.length(&1) > 80))
Data abstractions
-
def new, do: HashDict.new #见鬼,这里的defmodule里似乎定义的都是纯函数???没有OOP建模这回事???
-
defstruct a: nil, b: nil #嗯?
1. one_half = %Fraction{a: 1, b: 2}
2. Underneath, a struct instance is a special kind of a map.(one_half.b)
3. %Fraction{} = %{a: 1, b: 2} #匹配错误
4. one_quarter = %Fraction{one_half | b: 4}
5. ... 在struct上定义模式匹配约束看起来有点繁琐~ -
records(类似于tuple,但可以用名字访问元素)
-
Working with hierarchical data
1. 略 -
Polymorphism with protocols
1. defprotocol String.Chars do ... end #声明接口,但没有实现
2. defimpl String.Chars, for: Integer do
for Type可以是下列atoms:Tuple, Atom, List, Map, BitString, Integer, Float, Function, PID, Port, Reference
3. Elixir为加速协议动态派发的‘protocol consolidation’机制*
平台
并发原语
- ‘BEAM进程’
1. 当消息模式不匹配时,会被重新放到mailbox中?潜在的死循环?match all
other -> ...
2. 消息数据:大于64字节的二进制字符串不会深copy,而是通过特殊的共享堆传递
-
p142 async_query = fn(query_def) ->
caller_pid = self #这里的self实际上是一个函数?不是变量?
spawn(fn -> send(caller_pid, {:query_result, run_query.(query_def)}) #spawn里面如果直接使用self指的是派生子进程的pid? -
iex(4)> Enum.each(1..5, &async_query.("query #{&1}")) #传递函数对象需要加&符号?
-
见鬼,Erlang/Elixir里的循环全部使用尾递归的方式吗?
1. defp loop doreceive do ... end #通过message格式:{:tag, args...}
- after 5000 -> {:error, :timeout}
loop
-
有状态的服务:gen_server(不要自己写外层的递归循环控制!)
1. 这里的 DatabaseServer.run_async/2 例子连个回调都没有,异步查询结果实际上是通过轮询?
总不能每个IO操作都得异步吧?那样还不如用Go语言(它的调度器比较厉害)——看来我还是更喜欢JS/Python的语法
如果异步操作需要序列控制,那么可以使用单独的同步进程+队列控制???
2. server pool:
pool = 1..100 |> Enum.map(fn(_) -> DatabaseServer.start end)
1. 使用Enum.at(pool, :random.uniform(100) - 1)随机调度?
3. loop(state)
在消息传递架构下保持状态的做法让我想到RxJS里的例子(global state as snapshot)
-
注册进程:pid --> 本地别名
1. Process.register(pid, :some_name) #如何运行时动态构造一个atom? -
implicit yielding involves I/O operations:异步线程(负责IO操作,默认10个),看起来像是同步操作?
1. 请求内核poll:+K true
一般服务器进程
-
ServerProcess抽象
1. 同步call
2. 异步cast:handle_cast/2返回new_state,然后loop(new_state)? -
gen_server
1. OTP:gen_server supervisor application gen_event gen_fsm
2. defmodule KeyValueStore douse GenServer
3. iex(3)> GenServer.start(KeyValueStore, nil)
4. 实现下面3个回调:init/1, handle_cast/2, handle_call/3
init/1:返回{:ok, initial_state}或{:stop, some_reason}
handle_cast/2:返回{:noreply, new_state}
handle_call/3:返回{:reply, response, new_state}
5. 客户端进程调用GenServer.call/cast
-
OTP-compliant processes
1. Elixir抽象:tasks和agents -
Exercise: gen_server-powered to-do server
构建一个并发系统
-
mix工具
1. mix new todo 2. mix compile/test/...
3. 名字空间:todo/lib/todo/list.ex Todo.List? -
defmodule Todo.Cache do (模块名中可以带.有点不习惯)
1. def start do
GenServer.start(MODULE, nil) -
持久化数据
1. :erlang.term_to_binary/1 通用序列化?
2. !注意,在cast异步调用的场景下,请求id只能通过客户端生成,然后客户端使用此id查询cast的完成状态
反之,如果请求id需要服务器端生成的话,这个生成必然可能涉及io过程(除非是某个纯内存的id生成服务?),而这个不能避免复杂业务下的幂等调用要求。请求id可以用客户端的原始http(s)连接来标记。
3. 避免创建进程阻塞:send(self, :real_init) #defer init?
4. cast请求与响应处理的速度不匹配可能导致mailbox塞满溢出(分布式架构中的瓶颈)...?
pooling
数据库连接池:https://github.com/devinus/poolboy https://github.com/elixir-lang/ecto
缺陷容忍
-
3种类型的BEAM错误:error exit throw
1. 1/0 => ArithmeticError
2. UndefinedFunctionError
3. FunctionClauseError
4. raise("Something went wrong") => RuntimeError
5. 命名规范: File.open!("nonexistent_file") #对应的File.open版本可以返回{:error, :enoent}
6. exit("I'm done")
7. throw(:thrown_value) -
try do ... catch error_type, error_value -> ... end
1. iex(2)> try_helper.(fn -> raise("Something went wrong") end) #注意这里函数变量在调用时后面有个.号 -
defexception宏
-
Linking processes:spawn_link
1. 父进程崩溃将会导致子进程跟着崩溃(你通常不想这么做!而是希望子进程崩溃时父进程能收到通知吧?)
2. 'cross-process error propagation' -
Trapping exits
1. 在spawn_link子进程之前执行:Process.flag(:trap_exit, true) -
单向link:monitor_ref = Process.monitor(target_pid)
-
Supervisors(master进程,监控workers)
1. GenServer.start_link(MODULE, nil, name: :todo_cache)
2. Elixir:use Supervisor
processes = [worker(Todo.Cache, [])]
supervise(processes, strategy: :one_for_one) #如果一个child崩溃,重启一个新的
错误隔离(需要重读)
-
import Kernel, except: [send: 2] #import的时候防止名字冲突?Elixir的名字冲突检测只是在parser级别的?
1. 还有,函数就是通过名字和参数个数区分的?? -
restart策略:
1. one_for_one
2. simple_one_for_one
3. one_for_all 一个child崩溃,终止并重启整个children
4. rest_for_one 终止并重启所有列表位置在崩溃child后面的 -
“Let it crash”
-
data = case File.read(file_name(db_folder, key)) do
{:ok, contents} -> :erlang.binary_to_term(contents)
共享状态
-
ETS tables(内存K-V数据库)
1. type:?:set :ordered_set :bag :duplicate_bag
2. access权限::protected(默认) :public :private
3. 每个BEAM实例至多1400
4. 例::ets.new(:ets_page_cache, [:set, :named_table, :protected])
读缓存:case :ets.lookup(:ets_page_cache, key) do [{^key, cached}] -> cached #这里的^是什么意思? -
?A simple remedy is to introduce a write process per each distinct cacheable operation.
-
根据特定的value过滤条件遍历:
1. match patterns
iex(6)> :ets.match_object(todo_list, {:_, "Dentist"})
2. match specifications:Head/Guard/Result
:ets.select/2
:ets.fun2ms/1(编译期的,需要:@compile {:parse_transform, :ms_transform})
产品
组件
-
mix工具(怎么Laravel Elixir组件里也有这个名字?见鬼)
1. mix new hello_world --sup
2. iex(1)> :application.which_applications
3. mix.exs文件:(代码即配置)defmodule HelloWorld.Mixfile do
use Mix.Project
def project do... (下略)
4. An application is an OTP behaviour(OPT本身估计是有点复杂了?与JavaEE比较起来如何?)
5. 启动应用:iex -S mix
或:mix run --no-halt
6. Library applications:components that don’t need to create their own supervision tree
7. Application.start/2:as simple as **Todo.Supervisor.start_link**
-
依赖*
-
Building a web server
1. https://github.com/phoenixframework/phoenix https://github.com/extend/cowboy
2. 启动服务器:Plug.Adapters.Cowboy.http(MODULE, nil, port: 5454)
3. Plug:use Plug.Router
plug :match
plug :dispatch
post (宏?) "/add_entry" do ... endconn |> Plug.Conn.fetch_params |> add_entry |> respond
defp do_match("POST", ["add_entry"]) do
4. Cowboy:一个请求一个进程,这有点类似Go的架构
5. 使用Mnesia数据库*
- 管理应用环境
1. iex(1)> Application.put_env(:iex, :default_prompt, "Elixir>")
2. case Application.get_env(:todo, :port) do ... #命令行指定参数:$ iex --erl "-todo port 5454" -S mix
构建分布式系统
-
$ iex --sname node1@localhost
-
iex(node1@localhost)1> node
-
再启动一个,然后:iex(node2@localhost)1> Node.connect(:node1@localhost)
-
iex(node1@localhost)2> Node.list #列出连接到自己的节点(不包括自己)
1. iex(node1@localhost)3> Node.list([:this, :visible]) #cluster上所有的,包括自己 -
远程spawn一个进程,然后让它发消息给自己:
1. caller = self
2. Node.spawn(:node2@localhost, fn -> send(caller, {:response, 1+2}) end)- 实际不要这么做(传递lambda),而是用 Node.spawn/4 传递一个MFA参数(module,function,arguments)
-
Process discovery
1. 全局注册::global.register_name({:todo_list, "bob"}, self) #有类似ZooKeeper的分布式注册表机制?
2. <X.Y.Z> X非0肯定代表remote进程 -
进程组(?)
1. :pg2 ? -
其他的分布式服务:
1. 全局锁 :global.set_lock({:some_resource, self}) -
Building a fault-tolerant cluster
1. Network partitions,‘裂脑’
2. gproc:允许任意term作为注册alias,:global扩展到整个cluster
Todo.Supervisor.start_child/1返回{:error, {:already_started, pid}}:同样alias的process已经启动
3. 可靠的服务发现,同时降低网络通信开销
作者想说分布式一致哈希路由?
4. 实现数据库复制
新的store:{results, bad_nodes} = :rpc.multicall(MODULE, :store_local, [key, data], :timer.seconds(5)) #5秒超时
1. 这还不够,需要验证未超时的确实完成了数据存储请求
5. 测试系统:curl?
Mnesia doesn’t deal explicitly with network partitions and split-brainsc senarios, ...
6. Dealing with partitions(2个node不再能互相联系到对方)
CAP
1. CP系统:保证2*F+1个节点正常工作(quorum?)
2. AP系统:netsplit下继续工作,哪怕有可能接受了不能处理的订单
3. CA系统:没有意义!
- 网络层考虑
1. magical cookie,不要把集群暴露到公网(+防火墙?)
2. Hidden nodes?
3. the Erlang Port Mapper Daemon ( EPMD )
运行系统
-
$ elixir --erl "+P 2000000" -S mix run --no-halt #默认最大进程数是262144
-
elixir --detached --sname foo@localhost -S mix run --no-halt #后台运行 1. epmd -names
-
停止系统运行::init.stop
-
$ MIX_ENV=prod elixir -S mix run --no-halt
-
consolidate protocols:
1. MIX_ENV=prod mix compile.protocols 2. 使用优化后的.beam文件: MIX_ENV=prod elixir -pa _build/prod/consolidated -S mix run --no-halt -
OTP releases(这有点类似于Go)
1. Erlang社区:rebar (https://github.com/basho/rebar) or relx (https://github.com/erlware/relx)
2. exrm https://github.com/bitwalker/exrm
mix deps.get MIX_ENV=prod mix compile --no-debug-info
$ MIX_ENV=prod mix release -
Analyzing system behavior
1. IO.inspect
2. eprof, fprof, cprof
3. percept(分析系统的并发行为?)
4. 图形化的observer
5. ?Todo.Cache.server_process("bob") |> :sys.trace(true) #这个工具似乎有点意思,看着像是在单步调试
