201903 Distributed Fault Tolerant System Design 学习笔记 (2) A Tour of Erlang - xiaoxianfaye/Learning GitHub Wiki

1 What is Erlang?

Erlang is a programming language used to build massively scalable soft real-time systems with requirements on high availability. Some of its uses are in telecoms, banking, e-commerce, computer telephony and instant messagng. Erlang's runtime system has built-in support for concurrency, distribution and fault tolerance.

Erlang本身就是从电信领域诞生出的语言,天生就和电信软件开发比较亲和。

2 Hello world

-module(hello_world).
-compile(export_all).

hello() -> io:format("hello world~n").
1> c(hello_world).
{ok,hello_world}
2> hello_world:hello().
hello world
ok
  • module:每个Erlang文件声明了一个module。文件名必须和module名一模一样,再加上.erl后缀。 module名字一定是小写的。module是atom,要符合atom语法。
  • export_all:在调试过程中,在module中声明的所有方法都可以向外发布,在module外也可以访问这些方法。开发产品代码时,会写明哪些方法是需要export出去的,哪些不需要。
  • hello:方法。整个方法结束加上"."。
  • io:format:io是Erlang的built-in模块,加上":",写上方法名。
  • PATH环境变量要加一下Erlang的运行路径。

3 Values

Erlang has various value types, including integers, strings, floats, atoms, etc. These data types are also called terms.

-module(values).
-export([show/0]).
-record(dude, {name, age}).

println(What) -> io:format("~p~n", [What]).

show() ->
    println([2+3, 2#111, 16#ffa, 2.3e3]),
    println("Ole, ola!"),
    println(true and false),
    println(cool),
    println([1, 2, 3]),
    println({"Joe", 1, ok}),
    println(#{"age" => 20}),
    println(#dude{name="Joe", age=20}),
    println([<<"ABC">>, <<001>>]),
    println(make_ref()),
    Fun = fun() -> 10 end,
    println(Fun),
    println(self()).
1> c(values).
{ok,values}
2> values:show().
[5,7,4090,2.3e3]
"Ole, ola!"
false
cool
[1,2,3]
{"Joe",1,ok}
#{"age" => 20}
{dude,"Joe",20}
[<<"ABC">>,<<1>>]
#Ref<0.0.3.167>
#Fun<values.0.80322469>
<0.55.0>
ok
  • show/0:"方法名/入参个数"唯一确定一个方法。
  • ~p:占位,pretty print。Erlang根据数据的类型按照它认为比较漂亮的格式打印出来。
  • base#xxx:base进制的数。
  • Erlang中的字符串只是语法糖,其实是List数据类型。
  • atom:以小写字母开头,不加引号。经常用atom给数据类型做tag,true和false也是atom。
  • List:[], 元素类型可以不一样。
  • Tuple:{},元素类型可以不一样。列表是可变的,但Tuple一旦声明出来就不可变。
  • Map:#{key => value},Key-Value映射关系。
  • Record:#RecordName{key1=value1, key2=value2},value数据类型可以不同,像C语言中定义一个struct。
  • BitString/Binary:<<>>,位语义,电信协议开发用得很多。
  • Reference:只能调用built-in的make_ref()产生一个全局唯一的引用,可用于标识消息。
  • 匿名函数:Lambda。
  • self:Erlang的每个程序、方法都会在进程中执行,self()返回该方法所在的进程号。
3> self().
<0.55.0>
4> 1/0.
** exception error: an error occurred when evaluating an arithmetic expression
     in operator  '/'/2
        called as 1 / 0
5> self().
<0.64.0>

1/0这个错误的操作把当前Shell进程搞Crash了,Erlang监控了Shell,重新启动了一个新的Shell进程,所以两次self()返回了不同的结果。

4 Variables

Variables in Eralng are expressions. If a variable is bound to a value, the return value is that value.

-module(variables).
-compile(export_all).

showVariables() ->
    Name = "Joe Doe",
    io:format("Name: ~p ~n", [Name]),

    Age = 25,
    io:format("Age: ~p ~n", [Age]),

    IsMale = fun() -> true end,
    io:format("Male: ~p ~n", [IsMale()]),

    {A, [Head, Second|Rest]} = {1, [10, 20, 30, 40]},
    io:format("A =  ~p ~n", [A]),
    io:format("Head =  ~p ~n", [Head]),
    io:format("Second =  ~p ~n", [Second]),
    io:format("Rest =  ~p ~n", [Rest]),

    _ = 1,
    _ = 2.
1> c(variables).
{ok,variables}
2> variables:showVariables().
Name: "Joe Doe"
Age: 25
Male: true
A =  1
Head =  10
Second =  20
Rest =  [30,40]
2
3>
  • 在Erlang中,变量都是表达式。每个表达式都是要有值的。当变量和值做了绑定以后,表达式的值就是返回的值。
  • Erlang中的"=",对于变量来说,如果是变量的第一次赋值可以认为是变量的绑定,但其实是Match的过程。
  • 变量也可以是一个函数。
  • 在Erlang中,一次可以绑定很多个变量。
  • 变量以大写字母或下划线开头,下划线开头的变量是占位用的,不在意值。
  • 变量只能被绑定一次,即使以"_"开头,除了单独的"_"。
  • Pattern Match可用于对结果的拆分。
1> Name='a'.
a
2> Name='b'.
** exception error: no match of right hand side value b
3> _A=1.
1
4> _A=2.
** exception error: no match of right hand side value 2
5> _=1.
1
6> _=2.
2

5 Constants

Constants in Erlang can be created through the use of macros.

-module(constants).
-compile(export_all).

-define(N, 123).
-define(M, "what").
-define(SQUARED (X), X * X).

showConstants() ->
    io:format("N = ~p ~n", [?N]),
    io:format("M = ~p ~n", [?M]),
    io:format("~p ~n", [?SQUARED(5)]).
1> c(constants).
{ok,constants}
2> constants:showConstants().
N = 123
M = "what"
25
ok
  • 通过宏的方式定义常量,使用的时候前面加"?"。
  • 常量可以是任意值,甚至是函数。

6 Records

A record in Erlang is similar to a struct in C. It is typically used for storing a fixed number of elements. Record expressions are translated to tuple expressions during compilation.

-module(records).
-compile(export_all).

-record(person, {name, age, status = single}).

run() ->
    P1 = #person{name="Joe Doe", age=25},
    io:format("Created person ~p~n", [P1#person.name]),
    io:format("Record fields: ~p~n", [record_info(fields, person)]),
    io:format("Record size: ~p~n", [record_info(size, person)]).
1> c(records).
{ok,records}
2> records:run().
Created person "Joe Doe"
Record fields: [name,age,status]
Record size: 4
ok
  • 声明record的时候,字段可以有缺省值,没有定义缺省值的就是undefined。
  • record也是一个语法糖衣,本质是tuple。tuple必须严格按位置匹配,record是按名字匹配,不用关心顺序。
  • record_info():built-in函数,返回record的元信息。tuple的第一个字段是"person",所以record的size会多一个。

7 Maps

Maps are a type of data structures also known as dictionaries or associative arrays.

-module(map).
-compile(export_all).

write(String, Value) ->
    io:format("~p = ~p~n", [String, Value]).

run() ->
    M1 = #{name => "Joe Doe", age => 25},

    write("Map", M1),
    write("Name", maps:get(name, M1)),
    write("Degree", maps:get(degree, M1, defaultdegree)),

    Keyname = randomkey,
    case maps:find(Keyname, M1) of
        {ok, Value} ->
            write("Found value", Value);
        error ->
            write("No value found for key", Keyname)
    end.
1> c(map).
{ok,map}
2> map:run().
"Map" = #{age => 25,name => "Joe Doe"}
"Name" = "Joe Doe"
"Degree" = defaultdegree
"No value found for key" = randomkey
ok
  • Map本质上也是Tuple。
  • maps:get:如果key不存在,maps:get/2会抛异常,会把进程搞死。maps:get/3不会抛异常,返回一个key不存在时的缺省值。

8 Guards

A guard is a sequence of guard expressions, separated by the comma charactor (,).

The whole guard is true if all guard expressions will evaluate to true.

-module(guards).
-compile(export_all).

age(Age) when Age > 19 ->
    adult;
age(Age) when Age >= 13, Age =< 19 ->
    teen;
age(Age) when Age >=3, Age < 13 ->
    child;
age(Age) when Age >= 1, Age < 3 ->
    toddler.

is_single_digit(N) when N > 0, N < 10; N =< 0, N > -10 ->
    io:format("~p is a single digit integer ~n", [N]);
is_single_digit(N) ->
    io:format("Nope, ~p is not a single digit integer ~n", [N]).

run() ->
    io:format("A ~p year old is a ~p~n", [1, age(1)]),
    io:format("A ~p year old is a ~p~n", [4, age(4)]),
    io:format("A ~p year old is a ~p~n", [13, age(13)]),
    io:format("A ~p year old is a ~p~n", [23, age(23)]),

    is_single_digit(15),
    is_single_digit(4),
    is_single_digit(-5).
1> c(guards).
{ok,guards}
2> guards:run().
A 1 year old is a toddler
A 4 year old is a child
A 13 year old is a teen
A 23 year old is a adult
Nope, 15 is not a single digit integer
4 is a single digit integer
-5 is a single digit integer
ok
  • 卫语句用when开头。
  • 卫语句结合Pattern Match可以做精细的分支。
  • 卫语句由一系列卫表达式构成,逗号分隔的卫表达式相当于and,
  • 卫表达式也可以由一系列卫语句构成,分号分隔的卫语句相当于or。
  • 卫语句执行的时候是从上往下按顺序进行匹配。

9 If/Else

In Erlang, the 'if' is an expression which can have multiple branches.

-module(if_else).
-compile(export_all).

compare(X, Y) ->
    Result = if
        X > Y -> greater;
        X == Y -> equal;
        X < Y -> less
    end,
    io:format("~p is ~p than ~p ~n", [X, Result, Y]).

ascii(Letter) ->
    Code = if
        Letter =:= "A" -> 101;
        Letter =:= "B" -> 102;
        true -> unknown
    end,
    io:format("~p = ~p~n", [Letter, Code]).

run() ->
    compare(5, 1),
    ascii("A").
1> c(if_else).
{ok,if_else}
2> if_else:run().
5 is greater than 1
"A" = 101
ok
  • if/else中的if可以有多个分支,分支用分号分隔。
  • if中的分支如果一个都没有匹配,会报错,会Crash掉。所以,一般会加一个true分支,充当了else分支。
  • "="是Pattern Match,"=="和"=:="是等于。

10 Case of

You can use a 'Case ... of' expression to match against a sequence of patterns.

Unlike the 'if' expression, 'Case ... of' allows you use guards in the clauses.

-module(case_of).
-compile(export_all).

admit(Person) ->
    case Person of
        {male, Age} when Age >= 21 ->
            yes_with_cover;
        {female, Age} when Age >= 21 ->
            yes_no_cover;
        {male, _} ->
            no_boy_admission;
        {female, _} ->
            no_girl_admission;
        _ ->
            unknown
    end.

run() ->
    AdultMale = {male, 25},
    io:format(admit(AdultMale)),
    io:nl(),
    AdultFemale = {female, 25},
    io:format(admit(AdultFemale)),
    io:nl(),
    KidMale = {male, 5},
    io:format(admit(KidMale)),
    io:nl(),
    KidFemale = {female, 5},
    io:format(admit(KidFemale)),
    io:nl().
1> c(case_of).
{ok,case_of}
2> case_of:run().
yes_with_cover
yes_no_cover
no_boy_admission
no_girl_admission
ok
  • 结合Pattern Match。
  • "_"是default分支。"_"不能换成true,true就是匹配true。"_"也不能换成"{_, _}","{_, _}"表示必须是包含两个元素的Tuple。

11 Processes

Erlang was designed for massive concurrency. A process is created and terminated extremelly fast, that's why you can actually have thousands of them.

-module(processes).
-compile(export_all).

proc() -> io:format("I'm a process with id ~p~n", [self()]).

loop() -> loop().

run() ->
    spawn(fun() -> proc() end),
    spawn(processes, proc, []),
    spawn(?MODULE, proc, []),
    ok.
1> c(processes).
{ok,processes}
2> processes:run().
I'm a process with id <0.62.0>
I'm a process with id <0.63.0>
I'm a process with id <0.64.0>
ok
  • Erlang进程与操作系统进程的语义完全一样,进程之间是完全隔离的。但Erlang进程非常轻量(几百个字节),数百万进程的情况下,Context Switch开销代价很低。
  • Erlang有多个调度器,一个调度器对应操作系统的Thread,对应一个CPU。
  • 提供进程的语义。
  • loop():尾递归,没有调用堆栈了,所以不会溢出。

12 Pattern Matching

Like many other functional languages, Erlang has powerful pattern matching capabilities. Typically, a pattern (left side) is matched against a term (right side). Pattern matching can also occur in receive blocks, in which case the pattern is matched against the existing messages in a process queue. If the pattern match operation succeeds, then any unbound variables in the pattern will be bound.

-module(patterns).
-compile(export_all).

promote({ceo, male}) -> "man CEO now";
promote({ceo, female}) -> "woman CEO now";
promote({ceo, _}) -> "No CEO for you!".

demote(Arg) when is_tuple(Arg) ->
    case Arg of
        {ceo, male} -> "Fire the CEO";
        {ceo, female} -> "Fire the CEO";
        {ceo, _} -> "Cannot fire non existing CEO";
        _ -> "unknown pattern"
    end.

postman() ->
    receive
        {send, {Fr, To, _Content} = Pkg} ->
            io:format("~s sending something to ~s ~n", [Fr, To]),
            self() ! {recv, Pkg},
            postman();
        {recv, {To, Fr, Content} = _Pkg} ->
            io:format("~s got a ~s from ~s ~n", [Fr, Content, To]),
            postman();
        stop ->
            io:format("Shutting down postman ...~n")
    end.

is_even(Number) ->
    Type = try Number rem 2 of
        0 when is_number(Number) -> true;
        1 when is_number(Number) -> false
    catch
        _ErrType:_Err -> "I can't tell"
    end,
    io:format("Is ~p even? ~p~n", [Number, Type]).

head(List) ->
    [Head | _] = List,
    io:format("Head of list is: ~p~n", [Head]).

run() ->
    io:format("~p~n", [promote({ceo, female})]),
    io:format("~p~n", [promote({ceo, male})]),
    io:format("~p~n", [promote({ceo, other})]),

    io:format("~p~n", [demote({ceo, kid})]),

    Pid = spawn(?MODULE, postman, []),
    Pid ! {send, {"Joe", "Jane", "cool gift"}},

    timer:sleep(100),
    Pid ! stop,

    is_even(3),
    head([101, 201, 301]).
1> c(patterns).
{ok,patterns}
2> patterns:run().
"woman CEO now"
"man CEO now"
"No CEO for you!"
"Cannot fire non existing CEO"
Joe sending something to Jane
Jane got a cool gift from Joe
Is 3 even? false
Shutting down postman ...
Head of list is: 101
ok

13 Send/Receive

Processes in Erlang share no memory, hence they communicate via message passing.

Each process has a mailbox. The 'receive' block allows a process to do something wih the messages in its mailbox.

A process can receive different kinds of messages, then act on them.

To send a message to a process, use the '!' operator, prceded by the process id you want to send the message to.

这里的进程跟操作系统的进程具有一模一样的语义。

-module(send_recv).
-compile(export_all).

serve() ->
    receive
        Request ->
            io:format("Handling: ~s~n", [Request]),
            serve()
    end.

math() ->
    receive
        {add, X, Y} ->
            io:format("~p + ~p = ~p~n", [X, Y, X + Y]),
            math();
        {sub, X, Y} ->
            io:format("~p - ~p = ~p~n", [X, Y, X - Y]),
            math()
    end.

make_request(ServerId, Msg) ->
    ServerId ! Msg.

run() ->
    Pid = spawn(?MODULE, serve, []),
    make_request(Pid, request1),
    make_request(Pid, request2),

    timer:sleep(10),

    Pid2 = spawn(?MODULE, math, []),
    Pid2 ! {add, 1, 2},
    Pid2 ! {sub, 3, 2},
    ok.
1> c(send_recv).
{ok,send_recv}
2> send_recv:run().
Handling: request1
Handling: request2
1 + 2 = 3
3 - 2 = 1
ok

14 Links

Two processes can be linked to each other. A link is bidirectional.

如果仅仅只是能够起很多进程,但没有提供一些有效的机制能够做类似于像Supervisor这样的监控,还是不能很好地管理或应用程序。Erlang提供了很多这样的机制,都是基于两个最基础的监控机制——Link和Monitor。

用类似Erlang的思想开发我们的系统,把系统粒度分得很小。有一个业务由3个进程协作完成。在业务层面,这3个进程协作完成这样一件事情,每个进程都涵盖了一部分业务的状态。在业务的某种情况下,3个进程里只要有一个进程挂了,整个业务状态就完全不可控了,其他进程活着也没意义,这时重启这一个进程是没有意义的。重启一定是基于业务需要的,看业务上是否有意义,而不是能实现就这样重启。在业务层面上发现按业务重启是有意义的,业务的3个进程里重启某一个是没有意义的,那就把3个进程全部重启。Link就是帮我们解决这个问题的。我们把3个进程黏合在一起,一个进程挂了,3个进程一起重启。这就是我们业务上需要的语义,Link可以帮我们实现这个语义。Link只是一个工具,可以用工具来干这个事情。

Link是双向的,一个进程挂了会通知另外一个进程,可能会把另外一个进程也搞挂。我Link你,你就Link我了,不用你再Link我。

-module(links).
-compile(export_all).

child() ->
    io:format("I (child) have pid: ~p~n", [self()]),
    receive
        after 1000 ->
            io:format("I (child ~p) will die now!~n", [self()])
    end.

parent() ->
    Pid = spawn(links, child, []),
    link(Pid),
    io:format("I (parent) have pid: ~p~n", [self()]),
    io:format("I (parent) have a linked child: ~p~n", [Pid]),
    lists:foreach(
        fun(_) ->
            P = spawn_link(links, child, []),
            io:format("I (parent) have another linked child: ~p~n", [P])
        end,
        lists:seq(1, 4)
    ),

    process_flag(trap_exit, true),
    receive
        {'EXIT', Pid, Reason} ->
            timer:sleep(10),
            io:format("I (parent) have a dying child(~p), Reason: ~p~n", [Pid, Reason]),
            io:format("I (parent) will die too ...~n")
    end.

grandparent() ->
    Pid = spawn_link(links, parent, []),
    io:format("I (grandparent) have pid: ~p~n", [self()]),
    io:format("I (grandparent) have a linked child: ~p~n", [Pid]),

    process_flag(trap_exit, true),
    receive
        {'EXIT', Pid, Reason} ->
            timer:sleep(10),
            io:format("I (grandparent) have a dead child(~p), Reason: ~p~n", [Pid, Reason]),
            io:format("I (grandparent) will die too ...~n")
    end.

run() ->
    spawn(links, grandparent, []),
    timer:sleep(1100),
    ok.
  • child()中receive什么分支都没有,其实消息来了也不收,起来等1秒钟就结束了,就是sleep。
  • link()函数可以把两个进程link在一起。
  • spawn()和link()分开这种写法,可能会存在还没有来得及link,spawn的进程已经挂了,也没收到消息。spawn_link()把两者合在一起,作为一个原子操作。
  • A和B两个进程link在一起了,如果A进程crash,跟它link在一起的B进程会收到“{'EXIT', Pid, Reason}”消息。一般来说,如果Reason不是“normal”(normal的意思是进程正常死亡),B也会挂,B挂了以后会把这个消息继续传播给它link的那些进程。
  • 需要加一些控制,我们虽然link在一起了,你死了,不要先把我搞死,我要知道这个消息做一些事情,比如清理工作等。我不希望我被我自己link的进程搞死,要加上process_flag(trap_exit, true)这句话,这是一个built-in函数,意思是trap住exit信号,把trap_exit标志置成true,如果child挂了,parent不会挂,会收到exit信号,在收到'EXIT'消息的分支做一些事情。
  • 假设有一个Supvisor父进程,spawn并link了n个子进程,一个子进程死了,是否希望其他子进程死?这是你定义出来的,link只是工具。这里有很多模式:
    • one for All: 一个挂了,大家都挂
    • one for One: 一个挂了,其他的不挂
    • one for Rest:一个挂了,依赖它的才挂,不依赖它的不挂
  • link提供的机制就是如果你死了都死,但如果要不死的话,就要trap_exit。上面的程序中,parent跟child link在一起,如果child死了,先传给parent。如果没有process_flag(trap_exit, true)这句,那肯定就都死了。有这句的话,就可以控制住不再把信号继续传播。
  • link:比较强,双向,基本是幂等的,会收到一条Trap消息,不会收到很多条。
1> c(links).
{ok,links}
2> links:run().
I (grandparent) have pid: <0.62.0>
I (parent) have pid: <0.63.0>
I (child) have pid: <0.64.0>
I (grandparent) have a linked child: <0.63.0>
I (parent) have a linked child: <0.64.0>
I (parent) have another linked child: <0.65.0>
I (child) have pid: <0.65.0>
I (parent) have another linked child: <0.66.0>
I (child) have pid: <0.66.0>
I (parent) have another linked child: <0.67.0>
I (child) have pid: <0.67.0>
I (parent) have another linked child: <0.68.0>
I (child) have pid: <0.68.0>
I (child <0.64.0>) will die now!
I (child <0.65.0>) will die now!
I (parent) have a dying child(<0.64.0>), Reason: normal
I (child <0.66.0>) will die now!
I (child <0.67.0>) will die now!
I (child <0.68.0>) will die now!
I (parent) will die too ...
I (grandparent) have a dead child(<0.63.0>), Reason: normal
I (grandparent) will die too ...
ok

15 Monitors

Monitors are an alternative to links. Unlike links, which are bidirectional, monitors are unidirectional.

相对link,monitor轻量一些,而且是单向的监控机制。

-module(monitors).
-compile(export_all).

-define(TIMEOUT, 3000).

worker() ->
    receive
        do_work ->
            io:format("I (worker ~p) will work now~n", [self()]),
            worker()
        after ?TIMEOUT ->
            io:format("I (worker ~p) have no work to do~n", [self()]),
            io:format("I (worker ~p) will die now ...~n", [self()]),
            exit(no_activity)
    end.

parent() ->
    Pid = spawn(monitors, worker, []),
    register(worker, Pid),
    Reference = erlang:monitor(process, Pid),

    io:format("I (parent) have a new worker~p~n", [Pid]),
    ?MODULE ! {new_worker, Pid},
    receive
        {'DOWN', Reference, process, Pid, Reason} ->
            io:format("I (parent) My worker ~p died (~p)~n", [Pid, Reason]),
            parent()
    end.

loop() ->
    receive
        {new_worker, WorkerPid} ->
            timer:sleep(?TIMEOUT - 2000),
            WorkerPid ! do_work,
            loop()
    end.

start() ->
    Pid = spawn(monitors, loop, []),
    register(?MODULE, Pid),

    ParentPid = spawn(monitors, parent, []),
    register(parent, ParentPid),

    Ref = erlang:monitor(process, Pid),
    erlang:demonitor(Ref),

    timer:sleep(round(?TIMEOUT * 1.5)),
    exit(whereis(worker), fininshed),
    exit(whereis(parent), fininshed),
    exit(whereis(?MODULE), fininshed),
    ok.
  • 两个进程,一个是自己,一个是spawn出来的worker。
  • register(worker, Pid),给进程起个名字,Pid难记。
  • worker(),等一段时间看有没有事儿要做,没有就不消耗资源,就死。
  • parent(),希望parent所在的进程去monitor worker所在的进程,调用erlang:monitor(),第一个参数都填process,第二个参数是要监控的进程Pid。
  • erlang:monitor()返回一个Reference,当监控的Pid死了以后,会用返回的这个Reference发一个它死了的消息。多次调用erlang:monitor(),当Pid进程crash了以后,父进程会收到多个Monitor消息,每个Monitor消息带不同的Reference。任何两次调用都不可能返回同一个Reference。
  • monitor没有link那么强,被监控进程挂了,无论如何都不会把监控进程搞死。
  • monitor是一个单向的。worker死了,会告诉parent,但parent死了,worker不会受影响,这是一个单向的。worker死了以后,发了一个五元组的Tuple消息。
  • 'DOWN'也是atom。
1> c(monitors).
{ok,monitors}
2> monitors:start().
I (parent) have a new worker<0.64.0>
I (worker <0.64.0>) will work now
I (worker <0.64.0>) have no work to do
I (worker <0.64.0>) will die now ...
I (parent) My worker <0.64.0> died (no_activity)
I (parent) have a new worker<0.65.0>
ok

16 List Comprehension

List comprehension are used to generate new lists from existing ones.

来自数学集合论的概念。

-module(list_comp).
-compile(export_all).

run() ->
    L = [1, 2, 3, 4, 5],

    LL = [X * X || X <- L],
    io:format("~p~n", [L]),
    io:format("~p~n", [LL]),

    Evens = [X || X <- L, X rem 2 == 0],
    io:format("~p~n", [Evens]).
1> c(list_comp).
{ok,list_comp}
2> list_comp:run().
[1,2,3,4,5]
[1,4,9,16,25]
[2,4]
ok

17 Distributed Erlang

⚠️ **GitHub.com Fallback** ⚠️