Saat ini saya sedang mengerjakan penerjemah sederhana untuk bahasa pemrograman dan saya memiliki tipe data seperti ini:
data Expr
= Variable String
| Number Int
| Add [Expr]
| Sub Expr Expr
Dan saya memiliki banyak fungsi yang melakukan hal-hal sederhana seperti:
-- Substitute a value for a variable
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = go
where
go (Variable x)
| x == name = Number newValue
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
-- Replace subtraction with a constant with addition by a negative number
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = go
where
go (Sub x (Number y)) =
Add [go x, Number (-y)]
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Tetapi di masing-masing fungsi ini, saya harus mengulangi bagian yang memanggil kode secara rekursif hanya dengan perubahan kecil ke satu bagian dari fungsi. Apakah ada cara yang ada untuk melakukan ini secara lebih umum? Saya lebih suka tidak perlu menyalin dan menempel bagian ini:
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Dan hanya mengubah satu kasus setiap kali karena tampaknya tidak efisien untuk menduplikasi kode seperti ini.
Satu-satunya solusi yang saya bisa buat adalah memiliki fungsi yang memanggil fungsi pertama pada seluruh struktur data dan kemudian secara rekursif pada hasil seperti ini:
recurseAfter :: (Expr -> Expr) -> Expr -> Expr
recurseAfter f x =
case f x of
Add xs ->
Add $ map (recurseAfter f) xs
Sub x y ->
Sub (recurseAfter f x) (recurseAfter f y)
other -> other
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue =
recurseAfter $ \case
Variable x
| x == name -> Number newValue
other -> other
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd =
recurseAfter $ \case
Sub x (Number y) ->
Add [x, Number (-y)]
other -> other
Tapi saya merasa mungkin harus ada cara yang lebih sederhana untuk melakukan ini. Apakah saya melewatkan sesuatu?
Add :: Expr -> Expr -> Expr
alih-alihAdd :: [Expr] -> Expr
, dan singkirkanSub
sama sekali.recurseAfter
sedangana
menyamar. Anda mungkin ingin melihat anamorphisms danrecursion-schemes
. Yang sedang berkata, saya pikir solusi akhir Anda sesingkat mungkin. Beralih kerecursion-schemes
anamorfisme resmi tidak akan banyak menghemat.Jawaban:
Selamat, Anda baru saja menemukan kembali anamorfisme!
Ini kode Anda, diulang sehingga berfungsi dengan
recursion-schemes
paket. Sayangnya, ini tidak lebih pendek, karena kita membutuhkan pelat untuk membuat mesin bekerja. (Mungkin ada beberapa cara otomatis untuk menghindari boilerplate, misalnya menggunakan obat generik. Saya benar-benar tidak tahu.)Di bawah, Anda
recurseAfter
diganti dengan standarana
.Kami pertama-tama menentukan jenis rekursif Anda, serta fungsi yang merupakan titik tetapnya.
Kemudian kita menghubungkan keduanya dengan beberapa contoh sehingga kita bisa membuka
Expr
ke isomorfikExprF Expr
, dan melipatnya kembali.Terakhir, kami mengadaptasi kode asli Anda, dan menambahkan beberapa tes.
Alternatif bisa dengan
ExprF a
hanya mendefinisikan , dan kemudian diturunkantype Expr = Fix ExprF
. Ini menghemat beberapa pelat ketel di atas (misalnya dua contoh), dengan biaya harus menggunakanFix (VariableF ...)
alih-alihVariable ...
, serta analog dengan konstruktor lainnya.Satu lebih lanjut dapat meringankan yang menggunakan sinonim pola (dengan biaya sedikit lebih boilerplate, meskipun).
Pembaruan: Saya akhirnya menemukan alat automagic, menggunakan template Haskell. Ini membuat seluruh kode cukup pendek. Perhatikan bahwa
ExprF
functor dan dua instance di atas masih ada di bawah tenda, dan kita masih harus menggunakannya. Kami hanya menyimpan kerumitan karena harus mendefinisikannya secara manual, tetapi itu saja menghemat banyak usaha.sumber
Expr
secara eksplisit, bukan sesuatu sepertitype Expr = Fix ExprF
?Fix
+ konstruktor nyata. Menggunakan pendekatan terakhir dengan otomatisasi TH lebih baik, IMO.Sebagai pendekatan alternatif, ini juga merupakan kasus penggunaan khas untuk
uniplate
paket. Itu bisa menggunakanData.Data
obat generik alih-alih Template Haskell untuk menghasilkan boilerplate, jadi jika Anda membuatData
turunan untukExpr
:maka
transform
fungsi dariData.Generics.Uniplate.Data
menerapkan fungsi secara rekursif untuk setiap bersarangExpr
:Perhatikan bahwa secara
replaceSubWithAdd
khusus, fungsif
ini ditulis untuk melakukan subtitusi non-rekursif;transform
membuatnya rekursifx :: Expr
, jadi itu melakukan sihir yang sama untuk fungsi pembantu sepertiana
halnya dalam jawaban @ chi:Ini tidak lebih pendek dari solusi Template Haskell @ chi. Salah satu keunggulan potensial adalah
uniplate
menyediakan beberapa fungsi tambahan yang mungkin bermanfaat. Misalnya, jika Anda menggunakandescend
di tempattransform
, itu mengubah hanya langsung anak-anak yang dapat memberikan Anda kontrol atas mana rekursi terjadi, atau Anda dapat menggunakanrewrite
untuk kembali mengubah-hasil transformasi sampai Anda mencapai titik tetap. Salah satu kelemahan potensial adalah bahwa "anamorphism" terdengar jauh lebih keren daripada "uniplate".Program lengkap:
sumber