100言語Spedrdrun :エピソード81:エリクサー


Erlangはいくつかの強力な分散コンピューティング機能を備えたプラットフォームですが、それが使用する言語Erlangは本当にひどいです.Erlangの言語、その構文、その標準ライブラリ、その文字列処理、そのUnixの統合などのすべては、巨大な痛みです.
ある意味では、JavaとJVMの状況に似ていますが、それに対するすべてのファッショナブルな憎しみのために、Java言語はErlangの言語と同じくらい悪くどこにもありません.
それで、何人かの良い人々は正しいことをして、同じプラットホームのためにより良い言語―エリクソン―をつくりました.これはJavaを置き換える努力よりはるかに成功しました.and according to StackOverflow surveys, Elixir is currently more than twice as popular as Erlang , そして、Erlang VMの主な言語になりました.
一方、Kotlinと他の人はまだJava脅威を克服するために長い道のりを持っています.
Elixirはルビーに触発された構文を持っていますが、それは主に表面レベルの類似性です、そして、言語は非常に異なります.
Luaのエピソードのように、ルアーはブラジルから出てきた唯一の重要な技術だと言った.エリクサーは、かなりかなり成功したブラジルの言語です.

予言通り


Back in 2006 I wrote an Erlang review , 私が今このシリーズのためにしていることといくぶん似ています.

Here's my proposal:

  • Conform to the basic Unix conventions like ^D, man pages, and --help.
  • Throw away the old syntax and add something Python/Ruby-like. This isn't Lisp - weird syntax doesn't give you anything. Writing a decent parser in ANTLR is just one evening, and you don't have to throw away the old syntax, just provide an alternative. When you're at changing syntax, make it possible to access full language from the interpreter and limit the repeating yourself part a bit.
  • Write a decent standard library - real strings with Unicode (not this lists of integers hackery), regular expressions, arrays, hash tables and so on.
  • There is absolutely nothing that forces compilation by hand. Make it automatic by default.

Such changes won't interfere with the fault-safe distributed computing part even the tiniest bit. And if Erlang stays the way it is now, I think it is extremely unlikely to get out of its telecom niche.


そして、私が予言的な程度に正しかったことは、わかります.私がそれを書いた6年後にElixirは、^ Dを除いて全体のwishlistを届けました、そして、それはアーランよりずっと人気がありました.歴史の右側にいるのはいいことだ.

こんにちは、世界!


普通から始めましょう.こんにちは、世界!エリクシールで
#!/usr/bin/env elixir

IO.puts("Hello, World!")
$ ./hello.ex
Hello, World!

京大理


エリキシルはreplを持ちます、しかし、それは驚くべきでありません.Ctrl - Dは全く動作しません.また、Ctrl - Cを使っているなら、この奇妙なダイアログに行きます.
$ iex
Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit] [dtrace]

Interactive Elixir (1.13.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 2+2
4
iex(2)> ^C
BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
       (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution
a

ユニコード


あなたがあまりに近くないならば、ElixirはちょうどUnicodeを正当にサポートするようです.
#!/usr/bin/env elixir

IO.puts(String.length("Hello"))
IO.puts(String.length("Żółw"))
IO.puts(String.length("🍰"))
IO.puts(String.upcase("Żółw"))
IO.puts(String.downcase("Żółw"))
$ ./unicode.ex
5
4
1
ŻÓŁW
żółw

文字列とアーランVM


しかし、表面の下で、Erlang VMのストリング状況は、巨大な混乱です.以下のような文字列型がたくさんあります.
  • 整数のリンクリスト-どちらが「ストリング」であると思われていて、そうではないかを示す方法がない
  • 様々なエンコーディングを持つビット文字列
  • 原子(ルビー文字のように)
  • Erlang言語では、それは総災害です.Erlang REPLで単純な非ASCII文字列を作成する試みは以下の通りです.
    1> <<"Żółw">>.
    <<"{óBw">>
    2> <<"Żółw"/utf8>>.
    <<197,187,195,179,197,130,119>>
    3> "Żółw".
    [379,243,322,119]
    4> 'Żółw'.
    'Żółw'
    
    作業に最も近いと思われる最後のものは、実際には原子(シンボル)であり、文字列ではない.
    エリクサーは、これをよりよくするために、最善を尽くします.それはデフォルトでUTF 8ビット文字列にすべてのストリングを作ります.そして、それが利用可能なオプションの最も合理的であるので、適切な入力と出力を加えます.
    場合によっては、根底にあるErlang VMによって混乱した文字列問題に遭遇することがあります.
    iex(1)> [10,20]
    [10, 20]
    iex(2)> [10]
    '\n'
    iex(3)> [60,70,80]
    '<FP'
    iex(4)> 'Żółw'
    [379, 243, 322, 119]
    
    数字のいくつかのリストは、それらの内容に基づいて「文字列」として数のリストとして印刷されます.そしてそれはRPLとのための同じ規則ではないIO.puts . 全体的なエリクサーは非常に100 %修正することができない場合でも、Erlang VM文字列混乱を修正するために本当に行く.

    フィズバズ


    fizzbuzzをしましょう!
    #!/usr/bin/env elixir
    
    defmodule FizzBuzz do
      def fizzbuzz(n) do
        cond do
          rem(n, 15) == 0 -> "FizzBuzz"
          rem(n, 3) == 0 -> "Fizz"
          rem(n, 5) == 0 -> "Buzz"
          true -> n
        end
      end
    
      def loop(range) do
        range |> Enum.map(&fizzbuzz/1) |> Enum.each(&IO.puts/1)
      end
    end
    
    1..100 |> FizzBuzz.loop
    
    構文は漠然としたルビーのように見えます、しかし、詳細のどれも全く同じです.エリクサーの人々が一番好きなようなものは|> 演算子は、すでにジュリアのようないくつかの他の言語にその方法を行った.a |> b ジャストジャストb(a) , しかし、データが物事の束を流れている場合.a |> b |> c |> d は通常、d(c(b(a))) , または中間変数の使用.
    パイプへの関数が正しい位置に正しい引数を持っている場合にのみ動作します.パイプを入れるfoo(a, _) , でもそんなにfoo(_, a) . 他に何かのために、あなたは匿名の機能または若干の他のより複雑な構文を使用する必要があります.
    Rubyのような高度オブジェクト指向言語には、以下の必要があります|> AS. 既に正しい位置にある引数に対してのみ同じことを行いますself 引き数).エリクサーのrange |> Enum.map(&fizzbuzz/1) |> Enum.each(&IO.puts/1) ルビーに翻訳するrange.map{|x| fizzbuzz x}.each{|x| puts x} . 間の試合|> 連鎖と. 連鎖は正確ではありません、しかし、彼らは同様の状況の多くをカバーします.
    そしてもちろん、人々はより多くのために、両方の言語で、そして他のすべての言語でパイプラインを求め続けます.
    詳細は、一歩一歩.
  • すべての関数はモジュール内でなければなりませんdefmodule FizzBuzz do ... end - これはちょうど迷惑です、そして、Elixirは本当にトップレベルとREPLレベルを許可しなければなりませんdef s.
  • do ... end Rubyの慣習と全く一致しないので、もっとたくさんのことがありますdo ルビーより.また、使用することができますdo: ... for do ... end .
  • 関数は、すべての特定の&fizzbuzz/1 言及するfizzbuzz(n) , 我々は言うことができない&fizzbuzz (それが意味するようにfizzbuzz() ).
  • があるif and if/else , でもelsif , どれが非常に異常かcond これだけでなく
  • ほとんどは変数を変更できません
  • プロセス


    プロセスで何かをしようとしましょう.我々は緑の人々にプロセスを生成し、それを歓迎するメッセージを私たちが望んでいるの束を送信します.
    挨拶のプロセスも挨拶カウンタを保持します.
    #!/usr/bin/env elixir
    
    defmodule Greeter do
      def loop(counter) do
        receive do
          {:hello, msg} -> IO.puts("#{counter}: Hello, #{msg}!")
        end
        loop(counter + 1)
      end
    end
    
    greeter_pid = spawn(fn -> Greeter.loop(1) end)
    send(greeter_pid, {:hello, "World"})
    send(greeter_pid, {:hello, "Alice"})
    send(greeter_pid, {:hello, "Bob"})
    send(greeter_pid, {:hello, "Eve"})
    
    $  ./processes.ex
    1: Hello, World!
    2: Hello, Alice!
    3: Hello, Bob!
    4: Hello, Eve!
    
    ここで何が起こっているのか
  • spawn 新しいプロセスを作成し、pid (プロセスID )を返す
  • 我々は、そのプロセスにメッセージ(タプル)を送ることができますsend - PIDや他の識別子を使用できます
  • 状態を維持する主な方法は関数引数です.状態を更新したい場合は、更新された引数を使って自分自身を呼び出すだけですloop(counter + 1) does
  • receive do ... end プロセスのメールボックスから一つのメッセージを受け取り、
  • この場合、我々は気にかけるだけです{:hello, msg} メッセージ、そして私たちが何をするかは、ハローと、カウンタを印刷する
  • アカウント


    プロセスを少し複雑にしましょう.
    があるAccount これらの操作により、アカウントを維持するプロセス
  • Account.create(name, initial_balance)
  • {:deposit, value}
  • {:withdraw, value}
  • And Bank プロセス
  • Bank.create
  • {:create_account, name, initial_balance} メッセージ
  • {:transfer, from_name, to_name, amount} メッセージ
  • #!/usr/bin/env elixir
    
    defmodule Account do
      def loop(name, balance) do
        new_balance = receive do
          {:deposit, value} -> balance + value
          {:withdraw, value} -> balance - value
        end
        IO.puts("Balance for #{name} changed from #{balance} to #{new_balance}")
        loop(name, new_balance)
      end
    
      def create(name, initial_balance) do
        IO.puts("Account created for #{name} with initial balance #{initial_balance}")
        loop(name, initial_balance)
      end
    end
    
    defmodule Bank do
      def transfer(map, from_name, to_name, amount) do
        send(Map.get(map, from_name), {:withdraw, amount})
        send(Map.get(map, to_name), {:deposit, amount})
        loop(map)
      end
    
      def create_account(map, name, initial_balance) do
        pid = spawn(Account, :create, [name, initial_balance])
        map = Map.put(map, name, pid)
        loop(map)
      end
    
      def loop(map) do
        receive do
          {:create_account, name, initial_balance}
            -> create_account(map, name, initial_balance)
          {:transfer, from_name, to_name, amount}
            -> transfer(map, from_name, to_name, amount)
        end
      end
    
      def create do
        loop(%{})
      end
    end
    
    bank = spawn(Bank, :create, [])
    
    send(bank, {:create_account, "Alice", 1000})
    send(bank, {:create_account, "Bob", 2000})
    send(bank, {:create_account, "Eve", 200})
    send(bank, {:transfer, "Alice", "Bob", 500})
    send(bank, {:transfer, "Bob", "Eve", 220})
    
    それはすべての主流言語よりもはるかに非同期です.どのように意味があるのかに注意してください.
    $ ./accounts.ex
    Account created for Alice with initial balance 1000
    Account created for Eve with initial balance 200
    Account created for Bob with initial balance 2000
    Balance for Eve changed from 200 to 420
    Balance for Bob changed from 2000 to 2500
    Balance for Alice changed from 1000 to 500
    Balance for Bob changed from 2500 to 2280
    $ ./accounts.ex
    Account created for Bob with initial balance 2000
    Account created for Eve with initial balance 200
    Account created for Alice with initial balance 1000
    Balance for Alice changed from 1000 to 500
    Balance for Bob changed from 2000 to 2500
    Balance for Eve changed from 200 to 420
    Balance for Bob changed from 2500 to 2280
    
    プロセスが生成され、メッセージが送信されますが、彼らが処理されると謎であり、そのために準備する必要があります.
    詳細
  • Account - 全ての状態name ) と変数balance ) 引数として存在するloop - バランスを更新するには、新しい引数を使って呼び出します
  • Bank - 唯一の状態は、アカウントからアカウントプロセスIDへのマップですアカウント残高はアカウントプロセスにのみ存在する.If Bank それらを知りたかった.Account のような操作を実装する必要があります(get_balance ) そして、それはアカウントをpingする必要があります.もちろん、すべてがasyncであるなら、それは必ずしも最終的な状態ではないでしょう.
  • spawn(Account, :create, [name, initial_balance]) 別の構文spawn(fn -> Account.create(name, initial_balance) end) , 余分な匿名関数なしで
  • フィボナッチ


    ここで楽しいFibonacci すべての方法でプロセスを実装する.
    うまく行けばfib(20) プロセスはメッセージを受け取る{:fib, 18, 2584} and {:fib, 19, 4181} , そして、それが計算されたものを印刷し、その結果をメッセージとして送る{:fib, 20, 6765} 処理するfib21 and fib22 それで、チェーンはすべて継続することができます.
    しかし、このコードは非常に正しいことではありません.Process.register シンボル名をプロセスに関連付ける方法であり、プロセスIDに送ることができるように、そのような名前にメッセージを送ることができます.
    読者のための楽しい小さな運動として、どのような余分な手順は、レースの条件を削除するために撮影する必要がありますか?とno ,プロセスを後方へ(40..1 ) 答えではない.
    #!/usr/bin/env elixir
    
    defmodule Fib do
      def done(n, value) do
        IO.puts("fib(#{n}) = #{value}")
        send(:"fib#{n+1}", {:fib, n, value})
        send(:"fib#{n+2}", {:fib, n, value})
      end
    
      def waitforprevious(n, a, b) do
        {a, b} = receive do
          {:fib, m, value} -> cond do
            m == n - 1 -> {value, b}
            m == n - 2 -> {a, value}
            true -> {a, b}
          end
        end
    
        if (a == 0 or b == 0) do
          waitforprevious(n, a, b)
        else
          done(n, a + b)
        end
      end
    
      def calculate(n) do
        if n <= 2 do
          done(n, 1)
        else
          waitforprevious(n, 0, 0)
        end
      end
    end
    
    (1..40) |> Enum.each(fn n ->
      Process.register(spawn(Fib, :calculate, [n]), :"fib#{n}")
    end)
    
    いずれかのことが起こる-レースの条件によって打たれたか、しなかったいずれか
    $ ./fib.ex
    fib(1) = 1
    fib(2) = 1
    $ ./fib.ex
    fib(1) = 1
    fib(2) = 1
    fib(3) = 2
    fib(4) = 3
    fib(5) = 5
    fib(6) = 8
    fib(7) = 13
    fib(8) = 21
    fib(9) = 34
    fib(10) = 55
    fib(11) = 89
    fib(12) = 144
    fib(13) = 233
    fib(14) = 377
    fib(15) = 610
    fib(16) = 987
    fib(17) = 1597
    fib(18) = 2584
    fib(19) = 4181
    fib(20) = 6765
    fib(21) = 10946
    fib(22) = 17711
    fib(23) = 28657
    fib(24) = 46368
    fib(25) = 75025
    fib(26) = 121393
    fib(27) = 196418
    fib(28) = 317811
    fib(29) = 514229
    fib(30) = 832040
    fib(31) = 1346269
    fib(32) = 2178309
    fib(33) = 3524578
    fib(34) = 5702887
    fib(35) = 9227465
    fib(36) = 14930352
    fib(37) = 24157817
    fib(38) = 39088169
    fib(39) = 63245986
    fib(40) = 102334155
    
    ああ、実際の世界では、おそらくほとんどのレベルではなく、より高いレベルの機能を使用するspawn and send 閉じるこの動画はお気に入りから削除されています.

    あなたはエリクサーを使用する必要がありますか?


    あなたはエリキシル対Erlangを疑問に思っている場合は、それも本当のコンテストではない.決してErlangを使用して、エリクサーは厳密にすべての考えられる方法でより良いです.
    あなたがErlang VMを使用していなければならないかどうかの質問に関しては、それは間違いなく非常にユニークな同時実行モデルを提供して、本当に他のどのプラットホームででもそうすることができない方法で組織化されるソフトウェアにつながります.私は、すべての多くの問題がこの同時実行モデルを必要とすると思いません、しかし、あなたがそうするならば、Elixirはあなたにかなりきちんとした言語ですべてのこれらの能力への接近をします.

    コード


    All code examples for the series will be in this repository .
    Code for the Elixir episode is available here .