Erlang 入门
最近项目可能需要转 Erlang 做游戏服务端, 所以需要开始快速入门转开发, 这里补充些基础语法:
%%%-------------------------------------------------------------------
%%% @author MeteorCat
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 09. 7月 2024 下午 04:34
%%%-------------------------------------------------------------------
-module(app).
-author("MeteorCat").
%% API
-import(lists, [nth/2]).
-export([start/0]).
% 启动入口
start() ->
io:format("Service Started ~n"),
% 类型学习
learn_types(),
% 变量学习
learn_variables(),
% 概念构成
learn_syntax(),
% 运算学习
learn_operators(),
% 循环学习
learn_each(),
% 分支学习
learn_decision(),
% 匿名函数和匹配模式
learn_fun_and_maths(),
%% 这些都是基础, 还有包括函数导出和声明基本基础不需要赘述
%% 消化完这些知识点就是准备并发和进程构建等进阶
ok.
% 学习匿名函数和匹配模式
learn_fun_and_maths() ->
% 匿名函数基本上和其他语言一致 fun(xxx) -> end 定义
Fun1 = fun(V) -> [V] end,
io:format("Fun1: ~p~n", [Fun1]), % 这就是匿名函数定义
% 这里需要说明下 '|' 和 '++', '{}' 的匹配弹出和数据合并/解构匹配, 这是 Erlang 另外需要掌握的概念
ListDataA = [11, 22, 33, 44],
ListDataB = [55, 66, 77],
ListDataC = ListDataA ++ ListDataB,
io:format("ListDataC: ~p~n", [ListDataC]),% 合并两个数组
%% 合并字符串
StrA = "Hello",
StrB = "Wrold",
StrC = StrA ++ StrB,
io:format("StrC: ~p~n", [StrC]),% 合并两个字符串
%% 其他的类型不支持 ++ 合并操作, 但是内容是其他类型的列表都支持, 如下:
OtherA = [{a, 1}, {b, 2}, {c, 2, 33}],
OtherB = [{e, 24, xx}, {f, 2}, {g, 345, 663}],
OtherC = OtherA ++ OtherB,
io:format("OtherC: ~p~n", [OtherC]),% 本质上还是列表LIST合并
%% 本质上还是其他语言的 append 合并处理, 当然除了合并还带有剔除
FilterA = [11, 22, 44],
FilterB = [11, 33, 55],
FilterC = FilterA -- FilterB, % 注意过滤是前一个比较后一个剔除重复数据
io:format("FilterC: ~p~n", [FilterC]),
% 元组列表匹配
FilterOtherA = [{a, 2}, {a, 23, 3}, {a, 1}],
FilterOtherB = [{a, 1}, {a, 2}, {a, 30}],
FilterOtherC = FilterOtherA -- FilterOtherB,
io:format("FilterOtherC: ~p~n", [FilterOtherC]),
% 而字符串则是字符串替换
FilterStrA = "Hello.World",
FilterStrB = "Hello",
FilterStrC = FilterStrA -- FilterStrB,
io:format("FilterStrC: ~p~n", [FilterStrC]),
%% ++ 和 -- 是语法糖带来完美合并和剔除功能, 能够提高开发效率
% 另外还支持解构匹配, 就是对指定类型进行解构获取值
VarX = [222, 444, 555],
[Var1, _, _] = VarX, % 下划线代表忽略参数
io:format("Var1: ~p~n", [Var1]),
%% 多重匹配方式
{Port, _, [Name, _]} = {8080, "127.0.0.1", ["MeteorCat", 31]},
io:format("Port: ~p, Name: ~p~n", [Port, Name]),
% 字节流的解构就比较复杂, 需要单独另外在进阶讲解
%% 最后需要讲解的就是 | 弹出列表, 这种也是一种模式匹配
ListDataD = [11, 33, 66],
[Head | Tails] = ListDataD, % 弹出列表
io:format("Head: ~p, Tails: ~w, Data:~w~n", [Head, Tails, ListDataD]),
%% 注意, Erlang 还有空列表默认匹配
EmptyFun = fun([]) -> [123] end,
% EmptyFun(ListDataD), % 列表内部有数据, 没办法匹配成 [], 所以这里会报错(这里和其他语言有所区别)
EmptyFunRes = EmptyFun([]), % 只有提供空列表才会被匹配到
io:format("Res:~w~n", [EmptyFunRes]),
pass.
% 学习流程分支
learn_decision() ->
% Erlang的if是和传统概念不一样, 更多类似函数式的匹配模式, 注意最后条件是没有 ';' 结尾
% if Cond1 -> 代码A; Cond2 -> 代码B; Cond3 -> 代码C end
{MegaSec, Sec, MillSec} = erlang:timestamp(), % 先生成时间戳测试值
io:format("Timestamp: ~p~n", [[MegaSec, Sec, MillSec]]),
%% 单条件匹配, 注意最后结果后面是没有 ';' 分割
if (Sec rem 2) == 1 -> io:format("XXX~n");
true -> io:format("YYY~n")
end,
%% 多条件匹配
if Sec < 10000 -> io:format("XXX~n");
Sec < 50000 -> io:format("YYY~n");
Sec < 90000 -> io:format("ZZZ~n");
true -> io:format("EMPTY~n")
end,
%% 请注意现在不能叫分支判断, 而是应该匹配模式, 同时 if 还支持返回接收
VarIfDiv = if (Sec rem 2) == 1 -> Sec * Sec;
true -> Sec div 2
end,
io:format("Div Value: ~w~n", [VarIfDiv]),
% 除了 if-else 还带有 erlang 独有 switch 匹配
case Sec of
SecRes when (SecRes * SecRes) < (SecRes * 2) -> SecRes; % 复杂复配
SecRes2 when (SecRes2 > 10000) -> SecRes2 % 复杂比较匹配
% ...... 甚至多个复杂匹配
end,
% case 匹配更加适合复杂的分支匹配
ok.
% 列表解构
echo_tail([Head | Tail]) ->
io:format("~w - ~w~n", [Head, Tail]).
%% 学习循环
learn_each() ->
io:format("===============================~n"),
io:format("Learn Each~n"),
io:format("===============================~n"),
% 函数式语言是没有循环的, 更多是采用迭代器不断递归
% 所以作为函数式语言必须改变观念, 只接受数据迭代
% 这里 foreach 是内部高阶函数用于遍历
L_data = [10, 23, 40, 56, 320],
lists:foreach(fun(V) -> io:format("EachValue: ~w~n", [V]) end, L_data),
% 内部定义的高阶函数处理
% map 就是映射所有传递进来然后返回结果
L_map = lists:map(fun(V) -> V * 2 end, L_data),
lists:foreach(fun(V) -> io:format("[Map]EachValue: ~w~n", [V]) end, L_map),
% 甚至可以快速过滤需要的参数
L_filter = lists:filter(fun(V) -> V rem 2 == 0 end, L_data),
lists:foreach(fun(V) -> io:format("[Filter]EachValue: ~w~n", [V]) end, L_filter),
% 刚学习之后就建议学习递归获取列表概念, 后续业务会大量用到这些场景, 包括以下解构语法糖:
% 结构出头直到没办法解构
% echo_tail([Head | Tail]) ->
% io:format("~w - ~w~na", [Head, Tail]).
echo_tail([10, 30]), % 弹出首位
echo_tail([1]), % 弹出之后没东西返回 [] 列表
% Erlang 有很多语法糖需要去掌握
ok.
%%% 学习运算
learn_operators() ->
io:format("===============================~n"),
io:format("Learn Operators~n"),
io:format("===============================~n"),
% erlang 常规的运算和其他语言一致, 但是稍微有点区别而已
% 1. 算数运算
% 2. 关系运算
% 3. 逻辑运算
% 4. 位运算
% 常规运算
io:format("+: ~w~n", [1 + 2]), % 加
io:format("-: ~w~n", [10 - 2]), % 减
io:format("*: ~w~n", [2 * 2]), % 乘
io:format("/: ~w~n", [10 / 3]), % 除
io:format("rem: ~w~n", [12 rem 3]), % 取余
io:format("div: ~w~n", [14 div 5]), % 取整
% 关系运算
io:format("==: ~w~n", [13 rem 3 == 0]), % 对等
io:format("!=: ~w~n", [23 rem 4 /= 0]), % 这里和其他语言的 != 有差异
io:format(">: ~w~n", [35 div 5 > 12]), % 大于
io:format("=>: ~w~n", [42 div 5 >= 12]), % 大等于
io:format("<: ~w~n", [34 div 5 < 2]), % 小于
io:format("=<: ~w~n", [13 div 3 =< 2]), % 小等于, 这里也和其他语言 <= 差异, 留意符号位置
% 逻辑运算
% 这里还有其他动态语言都有的逻辑运算, 而不是采用类似 `&&` `||` 符号标识
io:format("or, ||: ~w~n", [(1 + 2 > 0) or (2 + 1 > 0)]),% 类似其他语言的 ||
io:format("and, &&: ~w~n", [(1 + 1 > 0) and (10 rem 5 == 0)]),% 类似其他语言的 &&
io:format("not, !: ~w~n", [not true]),% 类似其他语言的 ! 取反
io:format("xor, ^: ~w~n", [(not true) xor false]), % 异或取 false
% 位运算
% 这里就是比较高级的位进制换算, 这里实际上如果专注业务可能很少接触到
io:format("band, and: ~w~n", [1 band 1]), % 1 & 1 = 1
io:format("bor, or: ~w~n", [1 bor 0]), % 1 | 0 = 1
io:format("bxor, xor: ~w~n", [1 bxor 1]), % 1 ^ 1 = 0
io:format("bnot: ~w~n", [bnot (1 bxor 1)]),
% 这里都是之前常规的内容
ok.
%%% 学习变量
learn_variables() ->
io:format("===============================~n"),
io:format("Learn Variables~n"),
io:format("===============================~n"),
% 以 = 号做分界, 左边就是绑定对象, 右边就是绑定值
% 另外需要注意, 变量名必须以大写开头声明, 且被内部使用
N_value_left = 100,
N_value_right = 200,
% 注意, Erlang 不允许对值重新绑定, 所以需要自己重新绑定变量
% Rust 也有类型的赋值方案, 被称为值绑定的 ·所有权·
N_value_result = N_value_left + N_value_right,
io:format("Value Result: ~w~n", [N_value_result]),
% 现在回过头来说下数据格式化问题
% erlang 和其他语言格式化方式有所不同, 其他语言类型 "This is %s"
% 在 erlang 以 "~xxx" 声明需要格式化, 说下常规的格式化类型:
% ~n: 换行, ~f: 单浮点数, ~e: 双浮点数, ~w: 数值
% 其他类型比较特殊, 建议采用 ~p 指针打印
% format 第二个参数是数值用于带格式参数, erlang 并没有可变参数说法所以也就没办法可变传递
io:format("F:~f, D:~e, N:~w~n", [1.33, 23.4444, 244]),
ok.
%%% 学习语法概念
learn_syntax() ->
% 可以函数内部最后结尾都是 '.', 而如果没有终端则需要 ','
% 这种语法概念贴近人类自然语法, 逗号标识还没说完需要直到句号
VarText = "hello.world", % 逗号直到最后句号才是闭环
io:format("VarText ~w~n", [VarText]),
% 而在机器语言当中, 其实就是把函数内的调用栈连接成一段待执行代码
% 所以和其他语言随便定义函数之后可以调用不同, Erlang是不允许函数内重定义函数(匿名函数除外)
% 匿名函数则是可以随意声明, 也就是可以在函数内部来声明匿名函数
FunEcho = fun(Text) -> io:format("Hello.~w~n", [Text]) end,
FunEcho("MeteorCat"), % 可以就是临时定义临时使用
% 下面就是直接定义函数直接报错
% fun_other()->ok,
ok.
%%% 学习类型
learn_types() ->
io:format("===============================~n"),
io:format("Learn Types~n"),
io:format("===============================~n"),
% 数值类型, 数值是整数和浮点数合并
% 变量定义需要用大写开头
Type_num = 100,
%Type_num = 200, % Erlang内部值不允许再次赋值
% 注意只要内部定义就必须使用, 否则报错
% io:format 是格式化打印, 需要将常规语言格式化 \n 改成 ~n(换行) 转变习惯
% ~w~n 意义就是打印数值列表内部数据并且换行
io:fwrite("~w~n", [Type_num]),
% 原子量定义, erlang当中更加类型其他语言的 enum(枚举值)
% 同时原子量代表唯一性也就可以作为常量使用
% 原子量作用域是全局性的, 所以可以被全局检索
% 原子量定义都是统一用小写开头, 之后全部都是字母|数字|下划线|@等
atom@signal, % 原子量比较特殊, 定义之后可以不用赋值和使用
io:format("Atom : ~p~n", [atom@signal]),
%% 注意, 原子量没有任何意义, 只是个空标识, 如下处理
VarAtomFun = fun(Var, atom_value) -> Var * Var end, % 原子量没有任何意义, 只是传递个标识
VarAtomResult = VarAtomFun(20, atom_value), % 随便必须匹配原子量
%VarAtomResult = VarAtomFun(20, atom_other), % 外面随便给原子量会出现错误: Warning: no clause will ever match
io:format("AtomValue : ~p~n", [VarAtomResult]), % 所以鉴于这种特点, 很多第三方服务都采用 atom 量做标识传入
%% 比如 tcp 服务追加 [keepalive,delay] 等配置, 比如最下面 ok. 结尾是可以随便定义个原子量传递
% 布尔值和常规语言一致
Bool_status = (os:system_time() rem 2) > 0, % erlang 采用 rem 代表取余
io:format("Bool Status: ~p~n", [Bool_status]),
% 位串是 erlang 特有类型, 相当段字节序列每个字节代表8位二进制数
% 位串是很关键的类型, 用于网络编程二进制协议处理和文件操作
% 更进一步可以将位串看作其他语言的 bytes(byte[]) 类型
Bin_message = <<10, 20, 30>>, % 内部类型数量可以不断定义
io:format("Bytes: ~p~n", [Bin_message]), % 注意位串是必须掌握的概念
% binary_to_xxx 是 erlang 内部自带的二进制处理函数, 如下就是二进制位转 list 使用
Bin_list = binary_to_list(Bin_message),
io:format("Byte List: ~w~n", [Bin_list]),
% 元表和其他带有 Tuple 类型语言一致, 且声明之后固定不可变
% 用于保存不定量的数据, 比较常见的数据集合体
Tuple_data = {{meteorcat, 28}, {develop, 18}},
Tuple_data_len = tuple_size(Tuple_data),
io:format("Tuples(~w): ~p~n", [Tuple_data_len, Tuple_data]),
% 当然 erlang 也有简单的 KEY-Value 类型
% 和常规语言的 HashMap<K,V> 不同, erlang 是拆分出来 Map<K,V>, 后续需要做合并需要 Tuple<M> 或 List<M>
Map_entry_user = #{name => "MeteorCat", age => 28}, % 类似 json 映射对象
Map_entry_user_len = map_size(Map_entry_user),
io:format("Map Object(~w): ~p~n", [Map_entry_user_len, Map_entry_user]),
% 列表对象, 和其他常规编程语言移植
L_number = [10, "333", 10.3], % 动态语言特有的列表什么都能塞
io:format("List Object(~w): ~p~n", [length(L_number), L_number]),
% 以上就是基本需要了解类型, 其中如果高阶用到就是 ·位串·, 后续进阶这种类型息息相关
% 同时 erlang 也提供大量类型推断方法用于判断值类型
io:format("IsList: ~p~n", [erlang:is_list(L_number)]),
io:format("IsMap: ~p~n", [erlang:is_map(Map_entry_user)]),
io:format("IsNum: ~p~n", [erlang:is_number(Map_entry_user_len)]),
% 其他建议在进阶的时候不断学习
% 这里演示下无意义原子量传递
pass.
之后就是另外稍微比较基础的 模块 和 异常:
%%%-------------------------------------------------------------------
%%% @author MeteorCat
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%% 模块功能
%%% @end
%%% Created : 13. 7月 2024 下午 12:52
%%%-------------------------------------------------------------------
%%% 这里需要从头说起, 从 Erlang 的模块到 Actor 集群机制, 直到最后的挂起自己 Erlang 集群虚拟机
%% 这里就是定义模块名称, 在 erlang shell 之中, 就是通过 c(模块名) 来引入内部模块采用 模块名:函数() 调用
%% 比如定义并导出 main 方法, 那么就在 shell 中输入 c(stared:main), 注意如果是直接 erl 运行需要保证 erl 启动目录在 src 源代码编译 beam 文件
%% 编译的时候会将 erl 文件编译成 beam 文件, 同时 erlang 还支持动态 beam 加载模块从而实现不停机代码热更新, 这个高级特性用于游戏不停机业务方向
-module(stared).
%% 这里还有定义启动规范模板来让框架通用, 比较常用的就是如下:
%% application(应用) | gen_server(网络) | supervisor(进程管理器) | gen_fsm(有限状态机) | gen_event(通用事件处理)
%% -behaviour(). | -behavior(). 这里是美英语法不同; 两者都功能一样, 后续更加高级部分采用牵涉到这种, 总体来说就是通用的底层框架暴露实现回调实现
%% 这里就是声明作者信息
-author("MeteorCat").
%% 导出暴露对外的函数API
-export([
% 暴露模块内部的 函数/参数数量, 提供给外部调用
main/0,
except/0
]).
%%% =================================================
%% 下面是入门必须要看懂, 后续这种递归会大规模用到
%% Erlang 当中还有多重匹配重载模式, 首先需要理解学习就是函数递归
each(L, F) when erlang:is_function(F, 1) ->
case L of
%% 匹配弹出的值并且开始递归
%% 这里就是列表解构成 [首位值 | 后续列表], 并且引入下一层循环深层递归
[Head | Tail] -> [F(Head) | each_1(Tail, F)];
% 空匹配弹出
[] -> []
end.
%% 深层递归方法
%% 这里首先解构传入列表, [首位值|弹出首位列表], 之后不断移交给自己直到最后匹配成 []
each_1([Head | Tail], F) ->
[F(Head) | each_1(Tail, F)];
each_1([], _F) -> % 空匹配
[].
%% 这里注意中间函数采用 ';' , 这里其实就是合并成一条语句让其支持匹配重载
%%% =================================================
%% 入口函数定义, 并且暴露给外面, 这里最后返回 stop 原子量给外部接收者
main() ->
% 这里的头文件宏定义 ?MODULE 是当前模块名的 atom 量
% 剩下还有对应函数宏和文件名宏, 这些在做调试打印的时候十分有用
io:format("hello.world; Module: ~p, File: ~p, Line: ~p~n", [?MODULE, ?FILE, ?LINE]),
% 这里可以转成 atom 量成 list 对象
AtomModule = erlang:atom_to_list(?MODULE),
io:format("hello.world; Module: ~p~n", [AtomModule]),
% 测试遍历
LData = [111, 222, 333],
each(LData, fun(V) -> io:format("Value: ~w~n", [V]) end),
stop.
%% 重新定义个对外函数异常处理
except() ->
%% 这里测试将数值进行除零异常错误
%% Runtime terminating during boo
%% 以下代码就会触发这样错误:
%% erlang:throw(permission_denied),
%% 这样为了上面拦截异常终端, 则需要 try-catch-finally
try
% 这里模拟弹出异常, 这个原子量就是权限不足的异常
% erlang:throw(permission_denied)
% 这里弹出自定义异常错误的原子量
% erlang:error(io_error)
% 自定义怪异的原子量
erlang:exit(thisMyExit)
catch
% RuntimeException级别的错误:
error:Reason -> io:format("error:~p~n", [Reason]);
% throw: 和Java的 Throwable 较类似, 大多用于用户自己 throw 出异常到上层函数
throw:Reason -> io:format("throw:~p~n", [Reason]);
% Exception级别的错误: 进程停止异常, Exit 会在迫使进程崩溃的同时将进程退出的原因告诉其他进程, 一般不建议对Exit作捕获
exit:Reason -> io:format("exit:~p~n", [Reason])
after %% 等于 其他语言的 finally 代码块, 用于资源回收情况
io:format("Code Pass~n")
end,
stop.
注意: 必须完全理解函数传值匹配和递归调用, 无论自己编写库和第三方库都会用到大范围使用这种方式, 所以务必深度学习理解