Bagaimana kegigihan cocok dengan bahasa yang murni fungsional?

18

Bagaimana pola penggunaan penangan perintah untuk menangani ketekunan cocok dengan bahasa murni fungsional, di mana kami ingin membuat kode terkait IO setipis mungkin?


Saat menerapkan Desain Berbasis Domain dalam bahasa berorientasi objek, biasanya menggunakan pola Command / Handler untuk mengeksekusi perubahan status. Dalam desain ini, penangan perintah duduk di atas objek domain Anda, dan bertanggung jawab atas logika terkait kegigihan yang membosankan seperti menggunakan repositori dan menerbitkan peristiwa domain. Penangan adalah wajah publik dari model domain Anda; kode aplikasi seperti UI memanggil penangan ketika perlu mengubah status objek domain.

Sebuah sketsa dalam C #:

public class DiscardDraftDocumentCommandHandler : CommandHandler<DiscardDraftDocument>
{
    IDraftDocumentRepository _repo;
    IEventPublisher _publisher;

    public DiscardDraftCommandHandler(IDraftDocumentRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public override void Handle(DiscardDraftDocument command)
    {
        var document = _repo.Get(command.DocumentId);
        document.Discard(command.UserId);
        _publisher.Publish(document.NewEvents);
    }
}

The documentdomain objek bertanggung jawab untuk melaksanakan aturan bisnis (seperti "pengguna harus memiliki izin untuk membuang dokumen" atau "Anda tidak bisa membuang dokumen yang sudah dibuang") dan untuk menghasilkan peristiwa domain kita perlu untuk menerbitkan ( document.NewEventsakan menjadi IEnumerable<Event>dan mungkin akan berisi DocumentDiscardedacara).

Ini adalah desain yang bagus - mudah untuk diperluas (Anda dapat menambahkan kasus penggunaan baru tanpa mengubah model domain Anda, dengan menambahkan penangan perintah baru) dan agnostik mengenai bagaimana objek bertahan (Anda dapat dengan mudah menukar repositori NHibernate untuk Mongo repositori, atau tukar penerbit RabbitMQ dengan penerbit EventStore) yang membuatnya mudah untuk diuji menggunakan tipuan dan cemoohan. Itu juga mematuhi pemisahan model / tampilan - penangan perintah tidak tahu apakah itu digunakan oleh pekerjaan batch, GUI, atau API REST.


Dalam bahasa yang murni fungsional seperti Haskell, Anda dapat memodelkan penangan perintah secara kasar seperti ini:

newtype CommandHandler = CommandHandler {handleCommand :: Command -> IO Result)
data Result a = Success a | Failure Reason
type Reason = String

discardDraftDocumentCommandHandler = CommandHandler handle
    where handle (DiscardDraftDocument documentID userID) = do
              document <- loadDocument documentID
              let result = discard document userID :: Result [Event]
              case result of
                   Success events -> publishEvents events >> return result
                   -- in an event-sourced model, there's no extra step to save the document
                   Failure _ -> return result
          handle _ = return $ Failure "I expected a DiscardDraftDocument command"

Inilah bagian yang saya berjuang untuk mengerti. Biasanya, akan ada semacam kode 'presentasi' yang memanggil pemanggil perintah, seperti GUI atau API REST. Jadi sekarang kita memiliki dua lapisan dalam program kita yang perlu dilakukan IO - pengendali perintah dan tampilan - yang merupakan no-no besar di Haskell.

Sejauh yang saya bisa tahu, ada dua kekuatan yang berlawanan di sini: satu adalah model / pandangan pemisahan dan yang lainnya adalah kebutuhan untuk mempertahankan model. Perlu ada kode IO untuk mempertahankan model di suatu tempat , tetapi pemisahan model / view mengatakan bahwa kita tidak bisa meletakkannya di lapisan presentasi dengan semua kode IO lainnya.

Tentu saja, dalam bahasa "normal", IO dapat (dan memang) terjadi di mana saja. Desain yang baik menentukan bahwa berbagai jenis IO disimpan terpisah, tetapi kompiler tidak menegakkannya.

Jadi: bagaimana kita mendamaikan pemisahan model / tampilan dengan keinginan untuk mendorong kode IO ke tepi program, ketika model perlu dipertahankan? Bagaimana kita memisahkan dua jenis IO , tetapi masih jauh dari semua kode murni?


Pembaruan : Karunia berakhir dalam waktu kurang dari 24 jam. Saya tidak merasa bahwa salah satu jawaban saat ini telah menjawab pertanyaan saya sama sekali. Komentar @ Ptharien Flame tentang acid-statetampaknya menjanjikan, tapi itu bukan jawaban dan kurang detail. Aku benci poin-poin ini sia-sia!

Benjamin Hodgson
sumber
1
Mungkin akan membantu untuk melihat desain dari berbagai perpustakaan yang ada di Haskell; khususnya, acid-statetampaknya dekat dengan apa yang Anda gambarkan .
Ptharien Flame
1
acid-stateterlihat cukup bagus, terima kasih untuk tautannya. Dalam hal desain API tampaknya masih terikat IO; pertanyaan saya adalah tentang bagaimana kerangka kegigihan cocok dengan arsitektur yang lebih besar. Apakah Anda tahu ada aplikasi open-source yang digunakan di acid-statesamping lapisan presentasi, dan berhasil memisahkan keduanya?
Benjamin Hodgson
The Querydan Updatemonad yang cukup jauh dari IO, sebenarnya. Saya akan mencoba memberikan contoh sederhana dalam jawaban.
Flame Ptharien
Dengan risiko berada di luar topik, untuk setiap pembaca yang menggunakan pola Command / Handler dengan cara ini, saya sangat merekomendasikan untuk memeriksa Akka.NET. Model aktor terasa cocok di sini. Ada kursus yang bagus untuk itu di Pluralsight. (Aku bersumpah aku hanya seorang fanboy, bukan bot promosi.)
RJB

Jawaban:

6

Cara umum untuk memisahkan komponen di Haskell adalah melalui tumpukan transformator monad. Saya jelaskan ini secara lebih rinci di bawah ini.

Bayangkan kita sedang membangun sistem yang memiliki beberapa komponen skala besar:

  • komponen yang berbicara dengan disk atau basis data (submodel)
  • komponen yang melakukan transformasi pada domain kami (model)
  • komponen yang berinteraksi dengan pengguna (tampilan)
  • komponen yang menjelaskan hubungan antara tampilan, model, dan submodel (pengontrol)
  • komponen yang memulai seluruh sistem (driver)

Kami memutuskan bahwa kami perlu menjaga komponen-komponen ini secara longgar digabungkan untuk mempertahankan gaya kode yang baik.

Oleh karena itu kami mengkodekan setiap komponen kami secara polimorfik, menggunakan berbagai kelas MTL untuk memandu kami:

  • setiap fungsi dalam submodel adalah tipe MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState adalah representasi murni dari potret keadaan database atau penyimpanan kami
  • setiap fungsi dalam model adalah murni
  • setiap fungsi dalam tampilan adalah tipe MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState adalah representasi murni dari potret keadaan antarmuka pengguna kami
  • setiap fungsi dalam pengontrol bertipe MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • Perhatikan bahwa pengontrol memiliki akses ke status tampilan dan status submodel
  • driver hanya memiliki satu definisi,, main :: IO ()yang melakukan pekerjaan yang hampir sepele menggabungkan komponen lain ke dalam satu sistem
    • tampilan dan submodel perlu diangkat ke dalam tipe keadaan yang sama dengan pengontrol yang menggunakan zoomatau kombinator serupa
    • modelnya murni, dan bisa digunakan tanpa batasan
    • pada akhirnya, semuanya hidup dalam (tipe yang kompatibel dengan) StateT (DataState, UIState) IO, yang kemudian dijalankan dengan konten aktual dari database atau penyimpanan untuk diproduksi IO.
Api Ptharien
sumber
1
Ini adalah saran yang sangat bagus, dan persis apa yang saya cari. Terima kasih!
Benjamin Hodgson
2
Saya mencerna jawaban ini. Bisakah Anda menjelaskan peran 'submodel' dalam arsitektur ini? Bagaimana cara "berbicara dengan disk atau database" tanpa melakukan IO? Saya khususnya bingung tentang apa yang Anda maksud dengan " DataStateadalah representasi murni dari snapshot keadaan database atau penyimpanan kami". Mungkin Anda tidak bermaksud memuat seluruh database ke dalam memori!
Benjamin Hodgson
1
Saya benar-benar ingin melihat pemikiran Anda tentang implementasi C # dari logika ini. Jangan kira saya bisa menyuap Anda dengan suara keras? ;-)
RJB
1
@RJB Sayangnya, Anda harus menyuap tim pengembangan C # untuk memungkinkan jenis bahasa yang lebih tinggi, karena tanpa mereka arsitektur ini jatuh agak datar.
Ptharien's Flame
4

Jadi: bagaimana kita mendamaikan pemisahan model / tampilan dengan keinginan untuk mendorong kode IO ke tepi program, ketika model perlu dipertahankan?

Haruskah model ini perlu dipertahankan? Dalam banyak program, menyimpan model diperlukan karena keadaan tidak dapat diprediksi, operasi apa pun dapat mengubah model dengan cara apa pun, jadi satu-satunya cara untuk mengetahui keadaan model adalah dengan mengaksesnya secara langsung.

Jika, dalam skenario Anda, urutan peristiwa (perintah yang telah divalidasi dan diterima) selalu dapat menghasilkan negara, maka itu peristiwa yang perlu dipertahankan, tidak harus negara. Status selalu dapat dihasilkan dengan memutar ulang acara.

Karena itu, sering kali keadaan disimpan, tetapi hanya sebagai snapshot / cache untuk menghindari mengulang perintah, tidak sebagai data program penting.

Jadi sekarang kita memiliki dua lapisan dalam program kita yang perlu dilakukan IO - pengendali perintah dan tampilan - yang merupakan no-no besar di Haskell.

Setelah perintah diterima, acara dikomunikasikan ke dua tujuan (penyimpanan acara, dan sistem pelaporan) tetapi pada lapisan yang sama dari program.

Lihat Juga
Turunkan Acara Dengan
Saksama

FMJaguar
sumber
2
Saya kenal dengan event-sourcing (Saya menggunakannya dalam contoh saya di atas!), Dan untuk menghindari rambut yang membelah, saya masih mengatakan bahwa event-sourcing adalah pendekatan untuk masalah kegigihan. Bagaimanapun, sumber acara tidak meniadakan kebutuhan untuk memuat objek domain Anda di penangan perintah . Penangan perintah tidak tahu apakah objek berasal dari aliran peristiwa, ORM, atau prosedur yang tersimpan - itu hanya mendapatkannya dari repositori.
Benjamin Hodgson
1
Pemahaman Anda tampaknya menyatukan pandangan dan penangan perintah bersama untuk membuat beberapa IO. Pemahaman saya adalah bahwa pawang menghasilkan acara dan tidak memiliki minat lebih lanjut. Tampilan dalam contoh ini berfungsi sebagai modul terpisah (bahkan jika secara teknis dalam aplikasi yang sama), dan tidak digabungkan ke penangan perintah.
FMJaguar
1
Saya pikir kita mungkin berbicara dengan tujuan yang berlawanan. Ketika saya mengatakan 'view' saya berbicara tentang seluruh lapisan presentasi, yang mungkin berupa REST API, atau sistem model-view-controller. (Saya setuju bahwa pandangan harus dipisahkan dari model dalam pola MVC.) Saya pada dasarnya berarti "panggilan apa pun ke dalam penangan perintah".
Benjamin Hodgson
2

Anda mencoba menempatkan ruang pada aplikasi intensif IO Anda untuk semua aktivitas non-IO; sayangnya aplikasi CRUD tipikal seperti yang Anda bicarakan hanya sedikit melakukan IO.

Saya pikir Anda memahami denda pemisahan yang relevan, tetapi di mana Anda mencoba untuk menempatkan kode IO kegigihan di beberapa lapisan dari kode presentasi, fakta umum masalah ini ada di controller Anda di suatu tempat Anda harus memanggil Anda lapisan kegigihan, yang mungkin terasa terlalu dekat dengan presentasi Anda - tetapi itu hanya kebetulan bahwa jenis aplikasi itu memiliki sedikit hal lain untuk itu.

Presentasi dan kegigihan pada dasarnya merupakan keseluruhan dari jenis aplikasi yang saya pikir Anda uraikan di sini.

Jika Anda berpikir di kepala Anda tentang aplikasi serupa yang memiliki banyak logika bisnis yang kompleks dan pemrosesan data di dalamnya, saya pikir Anda akan dapat membayangkan bagaimana itu dipisahkan dengan baik dari IO presentasional dan kegigihan hal-hal IO sedemikian rupa sehingga perlu tahu apa-apa tentang baik. Masalah yang Anda miliki saat ini hanyalah masalah persepsi yang disebabkan oleh mencoba melihat solusi untuk masalah dalam jenis aplikasi yang tidak memiliki masalah untuk memulai.

Jimmy Hoffa
sumber
1
Anda mengatakan bahwa tidak apa-apa bagi sistem CRUD untuk memadukan kegigihan dan presentasi. Ini masuk akal bagi saya; Namun saya tidak menyebutkan CRUD. Saya secara khusus bertanya tentang DDD, di mana Anda memiliki objek bisnis yang menyihir interaksi yang kompleks, lapisan kegigihan (penangan perintah) dan lapisan presentasi di atas itu. Bagaimana Anda menjaga dua lapisan IO terpisah sambil mempertahankan pembungkus IO yang tipis ?
Benjamin Hodgson
1
NB, domain yang saya jelaskan dalam pertanyaan bisa sangat kompleks. Mungkin membuang dokumen konsep tunduk pada beberapa izin yang terlibat memeriksa, atau beberapa versi dari konsep yang sama mungkin perlu ditangani, atau pemberitahuan harus dikirim, atau tindakan perlu persetujuan oleh pengguna lain, atau draft melalui sejumlah tahap siklus hidup sebelum finalisasi ...
Benjamin Hodgson
2
@BenjaminHodgson Saya akan sangat menyarankan untuk tidak mencampuradukkan DDD atau metodologi desain OO lain yang inheren ke dalam situasi ini di kepala Anda, itu hanya akan membingungkan. Sementara ya Anda dapat membuat objek seperti bit dan bobbles di FP murni, pendekatan desain berdasarkan pada mereka tidak harus menjadi jangkauan pertama Anda. Dalam skenario yang Anda gambarkan, saya akan membayangkan seperti yang saya sebutkan di atas, sebuah pengontrol yang berkomunikasi antara dua IO dan kode murni: Presentasi IO masuk ke dan diminta dari pengontrol, pengontrol meneruskan berbagai hal ke bagian murni, dan ke bagian kegigihan.
Jimmy Hoffa
1
@BenjaminHodgson Anda bisa membayangkan gelembung di mana semua kode murni Anda hidup, dengan semua lapisan dan kegemaran yang Anda inginkan dalam desain apa pun yang Anda hargai. Titik masuk untuk gelembung ini akan menjadi bagian kecil yang saya sebut "controller" (mungkin salah) yang melakukan komunikasi antara presentasi, ketekunan, dan potongan murni. Dengan cara ini kegigihan Anda tidak tahu apa-apa tentang presentasi atau murni dan sebaliknya - dan ini membuat barang-barang IO Anda berada di lapisan tipis ini di atas gelembung sistem murni Anda.
Jimmy Hoffa
2
@BenjaminHodgson pendekatan "objek pintar" yang Anda bicarakan ini secara inheren merupakan pendekatan yang buruk untuk FP, masalah dengan objek pintar di FP adalah mereka berpasangan terlalu banyak dan menggeneralisasi terlalu sedikit. Anda berakhir dengan data dan fungsionalitas yang terkait dengannya, di mana FP lebih suka data Anda terlepas dari fungsionalitas sehingga Anda dapat mengimplementasikan fungsi Anda untuk digeneralisasikan dan mereka kemudian akan bekerja di berbagai jenis data. Bacalah jawaban saya di sini: programmers.stackexchange.com/questions/203077/203082#203082
Jimmy Hoffa
1

Sejauh yang saya bisa mengerti pertanyaan Anda (yang mungkin tidak saya lakukan, tetapi saya pikir saya akan memasukkan 2 sen saya), karena Anda tidak harus memiliki akses ke objek itu sendiri, Anda perlu memiliki database objek Anda sendiri yang dapat berakhir seiring waktu).

Idealnya objek itu sendiri dapat ditingkatkan untuk menyimpan keadaan mereka sehingga ketika mereka "diedarkan", prosesor perintah yang berbeda akan tahu apa yang mereka kerjakan.

Jika itu tidak mungkin, (icky icky), satu-satunya cara adalah dengan memiliki kunci seperti-DB yang umum, yang dapat Anda gunakan untuk menyimpan info di toko yang disetel agar dapat dibagikan di antara berbagai perintah - dan semoga, "buka" antarmuka dan / atau kode sehingga penulis perintah lain juga akan mengadopsi antarmuka Anda untuk menyimpan dan memproses informasi-meta.

Di bidang server file samba memiliki berbagai cara untuk menyimpan hal-hal seperti daftar akses dan aliran data alternatif, tergantung pada apa yang disediakan oleh OS host. Idealnya, samba di-host pada sistem file memberikan atribut yang diperluas pada file. Contoh 'xfs' di 'linux' - lebih banyak perintah menyalin atribut yang diperluas bersama dengan file (secara default, sebagian besar utilitas di linux "tumbuh" tanpa berpikir seperti atribut yang diperluas).

Solusi alternatif - yang bekerja untuk banyak proses samba dari pengguna yang berbeda yang beroperasi pada file umum (objek), adalah bahwa jika sistem file tidak mendukung melampirkan sumber daya secara langsung ke file seperti dengan atribut yang diperluas, menggunakan modul yang mengimplementasikan lapisan sistem file virtual untuk meniru atribut diperluas untuk proses samba. Hanya samba yang tahu tentang itu, tetapi memiliki keuntungan bekerja ketika format objek tidak mendukungnya, tetapi masih bekerja dengan beragam pengguna samba (lih. Pemroses perintah) yang melakukan beberapa pekerjaan pada file berdasarkan kondisi sebelumnya. Ini akan menyimpan informasi meta dalam database umum untuk sistem file yang membantu dalam mengontrol ukuran database (dan tidak

Mungkin tidak berguna bagi Anda jika Anda membutuhkan lebih banyak informasi khusus untuk implementasi yang sedang Anda kerjakan, tetapi secara konseptual, teori yang sama dapat diterapkan pada kedua set masalah. Jadi jika Anda mencari algoritma dan metode untuk melakukan apa yang Anda inginkan, itu mungkin bisa membantu. Jika Anda membutuhkan pengetahuan yang lebih spesifik dalam beberapa kerangka kerja tertentu, maka mungkin tidak begitu membantu ... ;-)

BTW - alasan saya menyebutkan 'kedaluwarsa' - adalah bahwa tidak jelas jika Anda tahu benda apa yang ada di luar sana dan berapa lama mereka bertahan. Jika Anda tidak memiliki cara langsung untuk mengetahui kapan suatu objek dihapus, Anda harus memotong metaDB Anda sendiri untuk mencegahnya mengisi dengan informasi meta lama atau kuno yang telah lama dihapus oleh pengguna.

Jika Anda tahu kapan objek kedaluwarsa / dihapus, maka Anda berada di depan permainan, dan dapat kedaluwarsa dari metaDB Anda pada saat yang sama, tetapi tidak jelas apakah Anda memiliki opsi itu.

Bersulang!

Astara
sumber
1
Bagi saya, ini sepertinya jawaban untuk pertanyaan yang sama sekali berbeda. Saya mencari saran mengenai arsitektur dalam pemrograman murni fungsional, dalam konteks desain berbasis domain. Bisakah Anda mengklarifikasi poin Anda?
Benjamin Hodgson
Anda bertanya tentang kegigihan data dalam paradigma pemrograman yang murni fungsional. Mengutip Wikipedia: "Fungsional murni adalah istilah dalam komputasi yang digunakan untuk menggambarkan algoritma, struktur data, atau bahasa pemrograman yang mengecualikan modifikasi destruktif (pembaruan) entitas dalam lingkungan program yang berjalan." ==== Menurut definisi, kegigihan data tidak relevan dan tidak digunakan untuk sesuatu yang tidak mengubah data. Sebenarnya tidak ada jawaban untuk pertanyaan Anda. Saya mencoba interpretasi yang lebih longgar tentang apa yang Anda tulis.
Astara