Advertisement

Elixir in Action-Manning 2015(读书笔记)

阅读量:

Elixir in Action

目录

语言

First steps

  1. Erlang/BEAM并不是最快的,但它的设计目标是支持大并发和可预测的性能

Building blocks

  1. It’s time to start learning about Elixir.(哈哈)

  2. 模块名中的.仅仅上逻辑上的管理方便,不代表模块之间的嵌套包含关系

  3. defmodule、def不是关键字,而是

  4. 管道:-5 |> abs |> Integer.to_string |> IO.puts(对应后面函数的第一个参数

  5. 参数默认值:x \ 0 (这个语法略显怪异,不过PHP里用\作为名字空间分隔符也蛮诡异的)

  6. 函数用参数个数区分,=> 没有可变参数 (C/C++ varargs ...)

  7. import IO:可以直接写puts,而不需要IO.puts全称

  8. alias IO, as: MyIO

  9. 模块属性(常量):@pi 3.1415926 (这些不同语言语法的细微之处真的很繁琐的)

  10. 类型规范:@spec area(number) :: number(Erlang没有静态类型系统??)
    1. @spec insert_at(list, integer, any) :: list

  11. 注释

  12. :an_atom(可以去掉:,改写为AnAtom)

  13. nil || false || 5 || true (返回5,这里的特性跟JS一样)

  14. Tuples
    1. person = {"Bob", 25}
    2. age = elem(person, 1)

  15. Lists
    1. Enum.at(prime_numbers, 4)
    2. List.replace_at(prime_numbers, 0, 11)

  16. Immutability(修改元素属性返回一个新元素)
    1. Elixir isn’t a pure functional language,(文件IO操作),但是变量不会被修改(只有‘重新bind’)

  17. 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)

  18. Binaries and bitstrings
    1. <<1, 2, 3>> 3字节数据
    2. 指定位宽度:<<1::4, 15::4>>
    3. <<1, 2>> <> <<3, 4>> 拼接2个比特串

  19. Strings
    1. ~S(Not escaped \n) ~S(Not interpolated #{3 + 0.14})
    2. Heredoc:""" ...... """

  20. First-class functions
    1. square = fn(x) -> x*x end
    2. square.(3) #匿名函数的调用加.
    3. 捕获操作:&IO.puts/1
    lambda = &(&1 * &2 + &3)

  21. Other built-in types
    1. 引用:Kernel.make_ref/0 (or make_ref ). 2^82
    2. pid
    3. port

  22. Higher-level types
    1. range = 1..2
    2. 关键词列表:days = [monday: 1, tuesday: 2, wednesday: 3] # 枚举?
    Keyword.get(days, :monday)
    用途:opts \ []

复制代码
3. HashDict
  1. IO lists(一颗深度嵌套的树,其中叶子节点是bytes数据)
    1. iolist = [[['H', 'e'], "llo,"], " worl", "d!"] #输出到IO设备时会被flatten?

  2. Operators:主要就是一个 |>
    1. +实际上是函数,Kernel.+

  3. Macros

  4. Special forms:&(...) for receive try

  5. 理解运行时:
    1. elixirc source.ex 2. iex(1)> apply(IO, :puts, ["Dynamic function call."]) #三元组MFA 3. elixir my_source.exs #解释执行???

Control flow

  1. 理解模式匹配
    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

  1. 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. 循环和迭代
    1. 数据不可变 => 没有for/while循环结构 ==> 使用尾递归 :accumulator

  2. 高阶函数
    1. Enum.reduce/3

  3. Comprehensions
    1. for x <- [1, 2, 3], y <- [1, 2, 3], (这里*)do: {x, y, x*y} #语法有点像Scala(多重循环只有一个for)

  • into: %{}
  1. 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

  1. def new, do: HashDict.new #见鬼,这里的defmodule里似乎定义的都是纯函数???没有OOP建模这回事???

  2. 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上定义模式匹配约束看起来有点繁琐~

  3. records(类似于tuple,但可以用名字访问元素)

  4. Working with hierarchical data
    1. 略

  5. 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’机制*

平台

并发原语

  1. ‘BEAM进程’
    1. 当消息模式不匹配时,会被重新放到mailbox中?潜在的死循环?match all
    other -> ...
复制代码
2. 消息数据:大于64字节的二进制字符串不会深copy,而是通过特殊的共享堆传递
  1. p142 async_query = fn(query_def) ->

    caller_pid = self #这里的self实际上是一个函数?不是变量?
    spawn(fn -> send(caller_pid, {:query_result, run_query.(query_def)}) #spawn里面如果直接使用self指的是派生子进程的pid?

  2. iex(4)> Enum.each(1..5, &async_query.("query #{&1}")) #传递函数对象需要加&符号?

  3. 见鬼,Erlang/Elixir里的循环全部使用尾递归的方式吗?
    1. defp loop do

    receive do ... end #通过message格式:{:tag, args...}

    • after 5000 -> {:error, :timeout}

    loop

  4. 有状态的服务: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)

  1. 注册进程:pid --> 本地别名
    1. Process.register(pid, :some_name) #如何运行时动态构造一个atom?

  2. implicit yielding involves I/O operations:异步线程(负责IO操作,默认10个),看起来像是同步操作?
    1. 请求内核poll:+K true

一般服务器进程

  1. ServerProcess抽象
    1. 同步call
    2. 异步cast:handle_cast/2返回new_state,然后loop(new_state)?

  2. gen_server
    1. OTP:gen_server supervisor application gen_event gen_fsm
    2. defmodule KeyValueStore do

    use 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
  1. OTP-compliant processes
    1. Elixir抽象:tasks和agents

  2. Exercise: gen_server-powered to-do server

构建一个并发系统

  1. mix工具
    1. mix new todo 2. mix compile/test/...
    3. 名字空间:todo/lib/todo/list.ex Todo.List?

  2. defmodule Todo.Cache do (模块名中可以带.有点不习惯)
    1. def start do
    GenServer.start(MODULE, nil)

  3. 持久化数据
    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

缺陷容忍

  1. 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)

  2. try do ... catch error_type, error_value -> ... end
    1. iex(2)> try_helper.(fn -> raise("Something went wrong") end) #注意这里函数变量在调用时后面有个.号

  3. defexception宏

  4. Linking processes:spawn_link
    1. 父进程崩溃将会导致子进程跟着崩溃(你通常不想这么做!而是希望子进程崩溃时父进程能收到通知吧?)
    2. 'cross-process error propagation'

  5. Trapping exits
    1. 在spawn_link子进程之前执行:Process.flag(:trap_exit, true)

  6. 单向link:monitor_ref = Process.monitor(target_pid)

  7. 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崩溃,重启一个新的

错误隔离(需要重读)

  1. import Kernel, except: [send: 2] #import的时候防止名字冲突?Elixir的名字冲突检测只是在parser级别的?
    1. 还有,函数就是通过名字和参数个数区分的??

  2. restart策略:
    1. one_for_one
    2. simple_one_for_one
    3. one_for_all 一个child崩溃,终止并重启整个children
    4. rest_for_one 终止并重启所有列表位置在崩溃child后面的

  3. “Let it crash”

  4. data = case File.read(file_name(db_folder, key)) do

    {:ok, contents} -> :erlang.binary_to_term(contents)

共享状态

  1. 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 #这里的^是什么意思?

  2. ?A simple remedy is to introduce a write process per each distinct cacheable operation.

  3. 根据特定的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})

产品

组件

  1. 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**
  1. 依赖*

  2. 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 ... end

    conn |> Plug.Conn.fetch_params |> add_entry |> respond

    defp do_match("POST", ["add_entry"]) do

复制代码
4. Cowboy:一个请求一个进程,这有点类似Go的架构
5. 使用Mnesia数据库*
  1. 管理应用环境
    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

构建分布式系统

  1. $ iex --sname node1@localhost

  2. iex(node1@localhost)1> node

  3. 再启动一个,然后:iex(node2@localhost)1> Node.connect(:node1@localhost)

  4. iex(node1@localhost)2> Node.list #列出连接到自己的节点(不包括自己)
    1. iex(node1@localhost)3> Node.list([:this, :visible]) #cluster上所有的,包括自己

  5. 远程spawn一个进程,然后让它发消息给自己:
    1. caller = self
    2. Node.spawn(:node2@localhost, fn -> send(caller, {:response, 1+2}) end)

    • 实际不要这么做(传递lambda),而是用 Node.spawn/4 传递一个MFA参数(module,function,arguments)
  6. Process discovery
    1. 全局注册::global.register_name({:todo_list, "bob"}, self) #有类似ZooKeeper的分布式注册表机制?
    2. <X.Y.Z> X非0肯定代表remote进程

  7. 进程组(?)
    1. :pg2 ?

  8. 其他的分布式服务:
    1. 全局锁 :global.set_lock({:some_resource, self})

  9. 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. 网络层考虑
    1. magical cookie,不要把集群暴露到公网(+防火墙?)
    2. Hidden nodes?
    3. the Erlang Port Mapper Daemon ( EPMD )

运行系统

  1. $ elixir --erl "+P 2000000" -S mix run --no-halt #默认最大进程数是262144

  2. elixir --detached --sname foo@localhost -S mix run --no-halt #后台运行 1. epmd -names

  3. 停止系统运行::init.stop

  4. $ MIX_ENV=prod elixir -S mix run --no-halt

  5. consolidate protocols:
    1. MIX_ENV=prod mix compile.protocols 2. 使用优化后的.beam文件: MIX_ENV=prod elixir -pa _build/prod/consolidated -S mix run --no-halt

  6. 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

  7. Analyzing system behavior
    1. IO.inspect
    2. eprof, fprof, cprof
    3. percept(分析系统的并发行为?)
    4. 图形化的observer
    5. ?Todo.Cache.server_process("bob") |> :sys.trace(true) #这个工具似乎有点意思,看着像是在单步调试

全部评论 (0)

还没有任何评论哟~