Yinelemeli toplam türleriyle uğraşırken kod çoğaltma nasıl azaltılır


50

Şu anda bir programlama dili için basit bir tercüman üzerinde çalışıyorum ve böyle bir veri türü var:

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr

Ve basit işler yapan birçok fonksiyonum var:

-- 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

Ancak bu işlevlerin her birinde, kodu yineleyen işlevin bir bölümüne sadece küçük bir değişiklikle tekrarlayan kısmı tekrarlamak zorundayım. Bunu daha genel olarak yapmanın mevcut bir yolu var mı? Bu bölümü kopyalayıp yapıştırmak istemem:

    go (Add xs) =
      Add $ map go xs
    go (Sub x y) =
      Sub (go x) (go y)
    go other = other

Ve her seferinde tek bir vakayı değiştirin, çünkü böyle bir kodu kopyalamak verimsiz görünüyor.

Gelebileceğim tek çözüm, önce tüm veri yapısında bir işlevi çağıran ve daha sonra bu sonuç üzerinde özyineli olarak bir işlevi çağıran bir işleve sahip olmaktır:

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

Ama muhtemelen bunu yapmanın daha basit bir yolu olması gerektiğini hissediyorum. Bir şey mi kaçırıyorum?


Kodun "kaldırılmış" bir sürümünü yapın. Ne yapacağınıza karar veren parametreleri (işlevleri) kullandığınız yer. Ardından, işlevleri kaldırılan sürüme geçirerek belirli işlevleri yapabilirsiniz.
Willem Van Onsem

Bence diliniz basitleştirilebilir. Define Add :: Expr -> Expr -> Expryerine Add :: [Expr] -> Exprve kurtulmak Subtamamen.
chepner

Ben sadece bu tanımı basitleştirilmiş bir versiyon olarak kullanıyorum; bu durumda işe yarayacak olsa da, dilin diğer bölümleri için de ifade listeleri
Scott

Gibi? Hepsi olmasa da zincirleme operatörlerin çoğu iç içe ikili operatörlere indirgenebilir.
chepner

1
Ben senin düşünüyorum recurseAfterIS anakılık. Anamorfizmlere bakmak isteyebilirsiniz ve recursion-schemes. Bununla birlikte, son çözümünüzün olabildiğince kısa olduğunu düşünüyorum. Resmi recursion-schemesanamorfizmlere geçmek fazla tasarruf etmeyecektir.
Chi

Yanıtlar:


38

Tebrikler, anamorfizmleri yeniden keşfettiniz!

İşte kodunuz, recursion-schemespaketle çalışması için yeniden ifade edildi . Ne yazık ki, daha kısa değil, çünkü makinelerin çalışması için bazı kazanlara ihtiyacımız var. (Örneğin, jenerik ilaç kullanmak gibi kazan plakasından kaçınmanın bazı otomajik bir yolu olabilir. Sadece bilmiyorum.)

Aşağıda, recurseAfterstandart ile değiştirilir ana.

Önce özyinelemeli türün yanı sıra sabit noktası olan işlevi tanımlarız.

{-# LANGUAGE DeriveFunctor, TypeFamilies, LambdaCase #-}
{-# OPTIONS -Wall #-}
module AnaExpr where

import Data.Functor.Foldable

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show)

data ExprF a
  = VariableF String
  | NumberF Int
  | AddF [a]
  | SubF a a
  deriving (Functor)

Sonra Exprizomorfik olarak ortaya çıkabilmemiz ExprF Exprve geri katlayabilmemiz için ikisini birkaç örnekle birleştiriyoruz .

type instance Base Expr = ExprF
instance Recursive Expr where
   project (Variable s) = VariableF s
   project (Number i) = NumberF i
   project (Add es) = AddF es
   project (Sub e1 e2) = SubF e1 e2
instance Corecursive Expr where
   embed (VariableF s) = Variable s
   embed (NumberF i) = Number i
   embed (AddF es) = Add es
   embed (SubF e1 e2) = Sub e1 e2

Son olarak, orijinal kodunuzu uyarlıyoruz ve birkaç test ekliyoruz.

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = ana $ \case
    Variable x | x == name -> NumberF newValue
    other                  -> project other

testSub :: Expr
testSub = substituteName "x" 42 (Add [Add [Variable "x"], Number 0])

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = ana $ \case
    Sub x (Number y) -> AddF [x, Number (-y)]
    other            -> project other

testReplace :: Expr
testReplace = replaceSubWithAdd 
   (Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4])

Bir alternatif ExprF asadece tanımlamak ve sonra türetmek olabilir type Expr = Fix ExprF. Bu kullanmak zorunda pahasına, yukarıda Demirbaş (örneğin, iki durum) bazı kaydeder Fix (VariableF ...)yerine Variable ..., hem de diğer kurucular için benzer.

Ayrıca, desen eşanlamlıları kullanmanın da (biraz daha kaynatma plakası pahasına) hafifletilebilir.


Güncelleme: Son olarak Haskell şablonu kullanarak otomajik aracı buldum. Bu, tüm kodu makul ölçüde kısaltır. Not o ExprFfunktoru ve yine de yukarıdaki iki durum kaputun altında var ve biz hala onları kullanmak zorunda. Bunları yalnızca manuel olarak tanımlamak zorunda kalmadan kurtarıyoruz, ancak bu tek başına çok çaba tasarrufu sağlıyor.

{-# LANGUAGE DeriveFunctor, DeriveTraversable, TypeFamilies, LambdaCase, TemplateHaskell #-}
{-# OPTIONS -Wall #-}
module AnaExpr where

import Data.Functor.Foldable
import Data.Functor.Foldable.TH

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show)

makeBaseFunctor ''Expr

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = ana $ \case
    Variable x | x == name -> NumberF newValue
    other                  -> project other

testSub :: Expr
testSub = substituteName "x" 42 (Add [Add [Variable "x"], Number 0])

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = ana $ \case
    Sub x (Number y) -> AddF [x, Number (-y)]
    other            -> project other

testReplace :: Expr
testReplace = replaceSubWithAdd 
   (Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4])

Gerçekten böyle bir Exprşey yerine açık bir şekilde tanımlamak zorunda type Expr = Fix ExprFmısınız?
chepner

2
@ chepner kısaca bir alternatif olarak bahsetmiştim. Her şey için çift yapıcı kullanmak zorunda kalmak biraz sakıncalıdır: Fix+ gerçek kurucu. TH otomasyonu ile son yaklaşımı kullanmak daha güzel, IMO.
Chi

19

Alternatif bir yaklaşım olarak, bu aynı zamanda uniplatepaket için tipik bir kullanım durumudur . Data.DataOrtak plakayı oluşturmak için Şablon Haskell yerine jenerikler kullanabilir , bu nedenle aşağıdakiler Dataiçin örnekler türetirseniz Expr:

import Data.Data

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show, Data)

daha sonra gelen transformişlev, Data.Generics.Uniplate.Dataiç içe yerleştirilmiş her bir öğeye özyinelemeli bir işlev uygular Expr:

import Data.Generics.Uniplate.Data

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = transform f
  where f (Variable x) | x == name = Number newValue
        f other = other

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = transform f
  where f (Sub x (Number y)) = Add [x, Number (-y)]
        f other = other

Belirtilmelidir ki, replaceSubWithAddözellikle, işlev folmayan yinelemeli ikame gerçekleştirmek için yazılıdır; transformbunu özyinelemeli hale getirir x :: Expr, bu yüzden ana@ chi'nin cevabındaki gibi yardımcı işlev için aynı büyüyü yapar:

> substituteName "x" 42 (Add [Add [Variable "x"], Number 0])
Add [Add [Number 42],Number 0]
> replaceSubWithAdd (Add [Sub (Add [Variable "x", 
                     Sub (Variable "y") (Number 34)]) (Number 10), Number 4])
Add [Add [Add [Variable "x",Add [Variable "y",Number (-34)]],Number (-10)],Number 4]
> 

Bu @ chi'nin Şablon Haskell çözümünden daha kısa değildir. Potansiyel bir avantaj, yardımcı olabilecek uniplatebazı ek fonksiyonlar sağlamasıdır. Örneğin, kullanmak durumunda descendyerine transform, sadece dönüştüren hemen tekrarlama olur nerede kontrolüne bırakabilir çocuk ya da kullanabileceğiniz rewritebir sabit noktaya ulaşana kadar dönüşümlerin yeniden dönüşümü sonucu var. Potansiyel bir dezavantaj, "anamorfizm" in "uniplate" den daha soğuk gelmesidir.

Tam program:

{-# LANGUAGE DeriveDataTypeable #-}

import Data.Data                     -- in base
import Data.Generics.Uniplate.Data   -- package uniplate

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show, Data)

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = transform f
  where f (Variable x) | x == name = Number newValue
        f other = other

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = transform f
  where f (Sub x (Number y)) = Add [x, Number (-y)]
        f other = other

replaceSubWithAdd1 :: Expr -> Expr
replaceSubWithAdd1 = descend f
  where f (Sub x (Number y)) = Add [x, Number (-y)]
        f other = other

main = do
  print $ substituteName "x" 42 (Add [Add [Variable "x"], Number 0])
  print $ replaceSubWithAdd e
  print $ replaceSubWithAdd1 e
  where e = Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)])
                     (Number 10), Number 4]
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.