Monad di Erlang


Di Habré, Anda dapat menemukan banyak publikasi yang mengungkap teori monad dan praktik penerapannya. Sebagian besar artikel ini diharapkan tentang Haskell. Saya tidak akan menceritakan kembali teori untuk yang ke-9 kalinya. Hari ini kita akan berbicara tentang beberapa masalah Erlang, cara untuk menyelesaikannya dengan monad, penggunaan sebagian fungsi dan gula sintaksis dari erlando - perpustakaan keren dari tim RabbitMQ.


Pendahuluan


Erlang memiliki kekekalan, tetapi tidak ada monad * . Namun berkat fungsionalitas parse_transform dan implementasi erlando dalam bahasa, masih ada kemungkinan menggunakan monad di Erlang.


Tentang kekebalan pada awal cerita, saya berbicara bukan secara kebetulan. Kekebalan hampir di mana-mana dan selalu merupakan salah satu ide utama Erlang. Kekebalan dan kemurnian fungsi memungkinkan Anda untuk fokus pada pengembangan fungsi tertentu dan tidak takut efek samping. Tetapi pendatang baru di Erlang, yang berasal dari Jawa atau Python, misalnya, merasa sangat sulit untuk memahami dan menerima ide-ide Erlang. Apalagi jika Anda mengingat sintaks Erlang. Mereka yang mencoba untuk mulai menggunakan Erlang mungkin mencatat keanehan dan kemandiriannya. Bagaimanapun, saya telah mengumpulkan banyak umpan balik dari pemula dan sintaksis "aneh" memimpin peringkat.


Erlando


Erlando adalah set ekstensi Erlang yang memberi kita:


  • Penggunaan sebagian / currying fungsi dengan potongan seperti Skema
  • Seperti Haskell, lakukan notasi
  • import-as - gula sintaksis untuk mengimpor fungsi dari modul lain.

Catatan: Saya mengambil contoh kode berikut untuk mengilustrasikan fitur erlando dari presentasi Matthew Sackman, sebagian melemahkannya dengan kode dan penjelasan saya.


Potong Abstrak


Langsung ke intinya. Pertimbangkan beberapa fungsi dari proyek nyata:


info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties{ expiry = calculate_msg_expiry(TTL)} end. 

Semua fungsi ini dirancang untuk menggantikan parameter menjadi ekspresi sederhana. Sebenarnya, ini adalah aplikasi parsial, karena beberapa parameter tidak akan diketahui sebelum panggilan. Bersama dengan fleksibilitas, fitur-fitur ini menambah derau pada kode kami. Dengan mengubah sedikit sintaks - dengan memasukkan cut - Anda dapat memperbaiki situasinya.


Nilai _


  • _ dapat digunakan dalam template
  • Cut memungkinkan Anda menggunakan _ pola luar
  • Jika di luar template, itu menjadi parameter untuk ekspresi di mana ia berada
  • Penggunaan banyak _ dalam ekspresi yang sama mengarah pada penggantian beberapa parameter dalam ekspresi ini
  • Cut bukan pengganti penutupan (kesenangan)
  • Argumen dievaluasi sebelum fungsi potong.

Cut menggunakan _ dalam ekspresi untuk menunjukkan di mana abstraksi harus diterapkan. Potong hanya membungkus level terdekat dalam ekspresi, tetapi potongan bersarang tidak dilarang.
Misalnya list_to_binary([1, 2, math:pow(2, _)]). list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). ke list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). tetapi tidak fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end. .


Kedengarannya agak membingungkan, mari kita menulis ulang contoh di atas menggunakan cut:


 info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). info_all(VHostPath, Items) -> map(VHostPath, info(_, Items)). 

 backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). backing_queue_timeout(State = #q{backing_queue = BQ}) -> run_backing_queue(BQ, _:timeout(_), State). 

 reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties { expiry = calculate_msg_expiry(TTL) } end. reset_msg_expiry_fun(TTL) -> _ #message_properties { expiry = calculate_msg_expiry(TTL) }. 

Urutan Perhitungan Argumen


Untuk mengilustrasikan urutan penghitungan argumen, pertimbangkan contoh berikut:


 f1(_, _) -> io:format("in f1~n"). test() -> F = f1(io:format("test line 1~n"), _), F(io:format("test line 2~n")). 

Karena argumen dievaluasi sebelum fungsi pemotongan, berikut ini akan ditampilkan:


 test line 2 test line 1 in f1 

Potong abstraksi dalam berbagai jenis dan pola kode


  • Tuples
     F = {_, 3}, {a, 3} = F(a). 
  • Daftar
     dbl_cons(List) -> [_, _ | List]. test() -> F = dbl_cons([33]), [7, 8, 33] = F(7, 8). 
  • Rekaman
     -record(vector, { x, y, z }). test() -> GetZ = _#vector.z, 7 = GetZ(#vector { z = 7 }), SetX = _#vector{x = _}, V = #vector{ x = 5, y = 4 } = SetX(#vector{ y = 4 }, 5). 
  • Kasing
     F = case _ of N when is_integer(N) -> N + N; N -> N end, 10 = F(5), ok = F(ok). 
  • Peta
     test() -> GetZ = maps:get(z, _), 7 = GetZ(#{ z => 7 }), SetX = _#{x => _}, V = #{ x := 5, y := 4 } = SetX(#{ y => 4 }, 5). 
  • Daftar yang Cocok dan Membangun Data Biner
     test_cut_comprehensions() -> F = << <<(1 + (X*2))>> || _ <- _, X <- _ >>, %% Note, this'll only be a /2 ! <<"AAA">> = F([a,b,c], [32]), F1 = [ {X, Y, Z} || X <- _, Y <- _, Z <- _, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2) ], [{3,4,5}, {4,3,5}, {6,8,10}, {8,6,10}] = lists:usort(F1(lists:seq(1,10), lists:seq(1,10), lists:seq(1,10))). 

Pro


  • Kode menjadi lebih kecil, oleh karena itu lebih mudah dipelihara.
  • Kode menjadi lebih sederhana dan lebih rapi.
  • Hilang kebisingan dari kesenangan.
  • Untuk pemula di Erlang, lebih mudah menulis fungsi Get / Set.

Cons


  • Peningkatan ambang entri untuk pengembang Erlang berpengalaman, sekaligus mengurangi ambang entri untuk pemula. Sekarang tim dituntut untuk memahami cut dan tahu satu sintaks lagi.

Lakukan notasi


Koma lunak adalah konstruksi pengikatan komputasi. Erlang tidak memiliki model perhitungan malas. Mari kita bayangkan apa yang akan terjadi jika Erlang malas seperti Haskell


 my_function() -> A = foo(), B = bar(A, dog), ok. 

Untuk menjamin urutan eksekusi, kita perlu menautkan penghitungan secara eksplisit dengan mendefinisikan koma.


 my_function() -> A = foo(), comma(), B = bar(A, dog), comma(), ok. 

Lanjutkan konversi:


 my_function() -> comma(foo(), fun (A) -> comma(bar(A, dog), fun (B) -> ok end)). 

Berdasarkan kesimpulan, koma / 2 adalah fungsi idiomatis >>=/2 . Monad hanya membutuhkan tiga fungsi: >>=/2 , return/1 dan fail/1 .
Semuanya akan baik-baik saja, tetapi sintaksinya buruk. Kami menerapkan transformer sintaksis dari erlando .


 do([Monad || A <- foo(), B <- bar(A, dog), ok]). 

Jenis-jenis Monads


Karena do-block sudah diparameterisasi, kita bisa menggunakan monad dari berbagai tipe. Di dalam do-block, masing-masing panggilan return/1 dan fail/1 dikerahkan ke Monad:return/1 dan Monad:fail/1 .


  • Identitas-monad.
    Monad identik adalah monad paling sederhana yang tidak mengubah jenis nilai dan tidak berpartisipasi dalam kontrol proses perhitungan. Itu diterapkan dengan transformer. Melakukan menghubungkan ekspresi - koma perangkat lunak yang dibahas di atas.


  • Mungkin-monad.
    Monad perhitungan dengan memproses nilai yang hilang. Mengaitkan parameter dengan perhitungan parameter adalah transfer parameter ke perhitungan, menghubungkan parameter tidak ada dengan perhitungan parameter adalah hasil yang tidak ada.
    Pertimbangkan contoh mungkin_m:


     if_safe_div_zero(X, Y, Fun) -> do([maybe_m || Result <- case Y == 0 of true -> fail("Cannot divide by zero"); false -> return(X / Y) end, return(Fun(Result))]). 

    Evaluasi ekspresi berhenti jika tidak ada yang dikembalikan.


     {just, 6} = if_safe_div_zero(10, 5, _+4) ## 10/5 = 2 -> 2+4 -> 6 nothing = if_safe_div_zero(10, 0, _+4) 

  • Kesalahan-monad.
    Mirip dengan maybe_m, hanya dengan penanganan kesalahan. Kadang-kadang prinsip let it crash tidak berlaku dan kesalahan harus ditangani pada saat itu terjadi. Dalam kasus ini, tangga dari kasing sering muncul dalam kode, misalnya, ini:


     write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], case make_binary(Data) of Bin when is_binary(Bin) -> case file:open(Path, Modes1) of {ok, Hdl} -> case file:write(Hdl, Bin) of ok -> case file:sync(Hdl) of ok -> file:close(Hdl); {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> E end; {error, _} = E -> E end. 

     make_binary(Bin) when is_binary(Bin) -> Bin; make_binary(List) -> try iolist_to_binary(List) catch error:Reason -> {error, Reason} end. 


Membaca ini tidak menyenangkan, sepertinya mi panggilan balik di JS. Error_m datang untuk menyelamatkan:


 write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], do([error_m || Bin <- make_binary(Data), Hdl <- file:open(Path, Modes1), Result <- return(do([error_m || file:write(Hdl, Bin), file:sync(Hdl)])), file:close(Hdl), Result]). make_binary(Bin) when is_binary(Bin) -> error_m:return(Bin); make_binary(List) -> try error_m:return(iolist_to_binary(List)) catch error:Reason -> error_m:fail(Reason) end. 

  • Daftar-monad.
    Nilai adalah daftar yang dapat diartikan sebagai beberapa hasil yang mungkin dari satu perhitungan. Jika satu perhitungan tergantung pada yang lain, maka perhitungan kedua dilakukan untuk setiap hasil yang pertama, dan hasil (perhitungan kedua) dikumpulkan dalam daftar.
    Perhatikan contoh dengan tiga kali lipat Pythagoras klasik. Kami menghitungnya tanpa monad:
     P = [{X, Y, Z} || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)]. 

Hal yang sama dengan list_m saja:


 P = do([list_m || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), monad_plus:guard(list_m, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)), return({X,Y,Z})]). 

  • Negara-monad.
    Monad komputasi stateful.
    Pada awal artikel, kami berbicara tentang kesulitan pemula ketika bekerja dengan keadaan variabel. Seringkali kode terlihat seperti ini:
     State1 = init(Dimensions), State2 = plant_seeds(SeedCount, State1), {DidFlood, State3} = pour_on_water(WaterVolume, State2), State4 = apply_sunlight(Time, State3), {DidFlood2, State5} = pour_on_water(WaterVolume, State4), {Crop, State6} = harvest(State5), ... 

Menggunakan transformator dan cut-notation, kode ini dapat ditulis ulang dalam bentuk yang lebih ringkas dan mudah dibaca:


 StateT = state_t:new(identity_m), SM = StateT:modify(_), SMR = StateT:modify_and_return(_), StateT:exec( do([StateT || StateT:put(init(Dimensions)), SM(plant_seeds(SeedCount, _)), DidFlood <- SMR(pour_on_water(WaterVolume, _)), SM(apply_sunlight(Time, _)), DidFlood2 <- SMR(pour_on_water(WaterVolume, _)), Crop <- SMR(harvest(_)), ... ]), undefined). 

  • Omega-monad.
    Mirip dengan list_m monad. Namun, perikop ini dibuat secara diagonal.

Penanganan kesalahan tersembunyi


Mungkin salah satu fitur favorit saya dari error_m monad. Di mana pun kesalahan terjadi, monad akan selalu mengembalikan {ok, Result} atau {error, Reason} . Contoh yang menggambarkan perilaku:


 do([error_m || Hdl <- file:open(Path, Modes), Data <- file:read(Hdl, BytesToRead), file:write(Hdl, DataToWrite), file:sync(Hdl), file:close(Hdl), file:rename(Path, Path2), file:delete(Path), return(Data)]). 

Import_as


Untuk camilan, kami memiliki sintaks import_as gula. Sintaks standar untuk atribut -import / 2 memungkinkan Anda untuk mengimpor fungsi dari yang lain ke modul lokal. Namun, sintaks ini tidak memungkinkan Anda untuk memberikan nama alternatif ke fungsi yang diimpor. Import_as menyelesaikan masalah ini:


 -import_as({my_mod, [{size/1, m_size}]}) -import_as({my_other_mod, [{size/1, o_size}]}) 

Ekspresi ini diperluas ke fungsi lokal nyata, masing-masing:


 m_size(A) -> my_mod:size(A). o_size(A) -> my_other_mod:size(A). 

Kesimpulan


Tentu saja, monads memungkinkan Anda untuk mengontrol proses perhitungan dengan metode yang lebih ekspresif, menghemat kode dan waktu untuk mendukungnya. Di sisi lain, mereka menambah kompleksitas ekstra untuk anggota tim yang tidak terlatih.


* - sebenarnya, di Erlang monad ada tanpa erlando. Ekspresi pemisah koma adalah konstruksi penghitungan linearisasi dan penghubung.


PS Baru-baru ini, perpustakaan erlando ditandai oleh penulis sebagai arsip. Saya menulis artikel ini lebih dari setahun yang lalu. Namun, seperti sekarang, di Habré tidak ada informasi tentang monad di Erlang. Untuk memperbaiki situasi ini, saya menerbitkan, meskipun terlambat, artikel ini.
Untuk menggunakan erlando di erlang> = 22, Anda harus memperbaiki masalah dengan erlang yang sudah usang: get_stacktrace / 0. Contoh perbaikan dapat ditemukan di garpu saya: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2


Terima kasih untuk waktu anda!

Source: https://habr.com/ru/post/id466697/


All Articles